mirror of https://github.com/home-assistant/core
Add JVC Projector integration (#84748)
* Initial commit of jvcprojector * Renamed domain * Initial commit * Support for v1.0.6 device api * Fixed failing test * Removed TYPE_CHECKING constant * Removed jvc brand * Removed constant rename * Renaming more constants * Renaming yet more constants * Improved config_flow tests * More changes based on feedback * Moved config_flow dependency * Removed default translation title * Removed translation file * Order manifest properly
This commit is contained in:
parent
bcbc8539a6
commit
6bbcf2f689
|
@ -178,6 +178,7 @@ homeassistant.components.iqvia.*
|
|||
homeassistant.components.isy994.*
|
||||
homeassistant.components.jellyfin.*
|
||||
homeassistant.components.jewish_calendar.*
|
||||
homeassistant.components.jvc_projector.*
|
||||
homeassistant.components.kaleidescape.*
|
||||
homeassistant.components.knx.*
|
||||
homeassistant.components.kraken.*
|
||||
|
|
|
@ -608,6 +608,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/juicenet/ @jesserockz
|
||||
/homeassistant/components/justnimbus/ @kvanzuijlen
|
||||
/tests/components/justnimbus/ @kvanzuijlen
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley
|
||||
/tests/components/jvc_projector/ @SteveEasley
|
||||
/homeassistant/components/kaiterra/ @Michsior14
|
||||
/homeassistant/components/kaleidescape/ @SteveEasley
|
||||
/tests/components/kaleidescape/ @SteveEasley
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
"""The jvc_projector integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import JvcProjectorDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.REMOTE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up integration from a config entry."""
|
||||
device = JvcProjector(
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await device.connect(True)
|
||||
except JvcProjectorConnectError as err:
|
||||
await device.disconnect()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to {entry.data[CONF_HOST]}"
|
||||
) from err
|
||||
except JvcProjectorAuthError as err:
|
||||
await device.disconnect()
|
||||
raise ConfigEntryAuthFailed("Password authentication failed") from err
|
||||
|
||||
coordinator = JvcProjectorDataUpdateCoordinator(hass, device)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
async def disconnect(event: Event) -> None:
|
||||
await device.disconnect()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await hass.data[DOMAIN][entry.entry_id].device.disconnect()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
|
@ -0,0 +1,129 @@
|
|||
"""Config flow for the jvc_projector integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError
|
||||
from jvcprojector.projector import DEFAULT_PORT
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.util.network import is_host_valid
|
||||
|
||||
from .const import DOMAIN, NAME
|
||||
|
||||
|
||||
class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for the JVC Projector integration."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle user initiated device additions."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
port = user_input[CONF_PORT]
|
||||
password = user_input.get(CONF_PASSWORD)
|
||||
|
||||
try:
|
||||
if not is_host_valid(host):
|
||||
raise InvalidHost
|
||||
|
||||
mac = await get_mac_address(host, port, password)
|
||||
except InvalidHost:
|
||||
errors["base"] = "invalid_host"
|
||||
except JvcProjectorConnectError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except JvcProjectorAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
await self.async_set_unique_id(format_mac(mac))
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: host, CONF_PORT: port, CONF_PASSWORD: password}
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=NAME,
|
||||
data={
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
CONF_PASSWORD: password,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Optional(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult:
|
||||
"""Perform reauth on password authentication error."""
|
||||
self._reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
assert self._reauth_entry
|
||||
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
host = self._reauth_entry.data[CONF_HOST]
|
||||
port = self._reauth_entry.data[CONF_PORT]
|
||||
password = user_input[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
await get_mac_address(host, port, password)
|
||||
except JvcProjectorConnectError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except JvcProjectorAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self._reauth_entry,
|
||||
data={CONF_HOST: host, CONF_PORT: port, CONF_PASSWORD: password},
|
||||
)
|
||||
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class InvalidHost(Exception):
|
||||
"""Error indicating invalid network host."""
|
||||
|
||||
|
||||
async def get_mac_address(host: str, port: int, password: str | None) -> str:
|
||||
"""Get device mac address for config flow."""
|
||||
device = JvcProjector(host, port=port, password=password)
|
||||
try:
|
||||
await device.connect(True)
|
||||
finally:
|
||||
await device.disconnect()
|
||||
return device.mac
|
|
@ -0,0 +1,5 @@
|
|||
"""Constants for the jvc_projector integration."""
|
||||
|
||||
NAME = "JVC Projector"
|
||||
DOMAIN = "jvc_projector"
|
||||
MANUFACTURER = "JVC"
|
|
@ -0,0 +1,62 @@
|
|||
"""Data update coordinator for the jvc_projector integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from jvcprojector import (
|
||||
JvcProjector,
|
||||
JvcProjectorAuthError,
|
||||
JvcProjectorConnectError,
|
||||
const,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import NAME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
INTERVAL_SLOW = timedelta(seconds=60)
|
||||
INTERVAL_FAST = timedelta(seconds=6)
|
||||
|
||||
|
||||
class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
"""Data update coordinator for the JVC Projector integration."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, device: JvcProjector) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=_LOGGER,
|
||||
name=NAME,
|
||||
update_interval=INTERVAL_SLOW,
|
||||
)
|
||||
|
||||
self.device = device
|
||||
self.unique_id = format_mac(device.mac)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, str]:
|
||||
"""Get the latest state data."""
|
||||
try:
|
||||
state = await self.device.get_state()
|
||||
except JvcProjectorConnectError as err:
|
||||
raise UpdateFailed(f"Unable to connect to {self.device.host}") from err
|
||||
except JvcProjectorAuthError as err:
|
||||
raise ConfigEntryAuthFailed("Password authentication failed") from err
|
||||
|
||||
old_interval = self.update_interval
|
||||
|
||||
if state[const.POWER] != const.STANDBY:
|
||||
self.update_interval = INTERVAL_FAST
|
||||
else:
|
||||
self.update_interval = INTERVAL_SLOW
|
||||
|
||||
if self.update_interval != old_interval:
|
||||
_LOGGER.debug("Changed update interval to %s", self.update_interval)
|
||||
|
||||
return state
|
|
@ -0,0 +1,38 @@
|
|||
"""Base Entity for the jvc_projector integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from jvcprojector import JvcProjector
|
||||
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, NAME
|
||||
from .coordinator import JvcProjectorDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JvcProjectorEntity(CoordinatorEntity[JvcProjectorDataUpdateCoordinator]):
|
||||
"""Defines a base JVC Projector entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: JvcProjectorDataUpdateCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_unique_id = coordinator.unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.unique_id)},
|
||||
name=NAME,
|
||||
model=self.device.model,
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
|
||||
@property
|
||||
def device(self) -> JvcProjector:
|
||||
"""Return the device representing the projector."""
|
||||
return self.coordinator.device
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"domain": "jvc_projector",
|
||||
"name": "JVC Projector",
|
||||
"codeowners": ["@SteveEasley"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/jvc_projector",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==1.0.6"]
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
"""Remote platform for the jvc_projector integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from jvcprojector import const
|
||||
|
||||
from homeassistant.components.remote import RemoteEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import JvcProjectorEntity
|
||||
|
||||
COMMANDS = {
|
||||
"menu": const.REMOTE_MENU,
|
||||
"up": const.REMOTE_UP,
|
||||
"down": const.REMOTE_DOWN,
|
||||
"left": const.REMOTE_LEFT,
|
||||
"right": const.REMOTE_RIGHT,
|
||||
"ok": const.REMOTE_OK,
|
||||
"back": const.REMOTE_BACK,
|
||||
"mpc": const.REMOTE_MPC,
|
||||
"hide": const.REMOTE_HIDE,
|
||||
"info": const.REMOTE_INFO,
|
||||
"input": const.REMOTE_INPUT,
|
||||
"cmd": const.REMOTE_CMD,
|
||||
"advanced_menu": const.REMOTE_ADVANCED_MENU,
|
||||
"picture_mode": const.REMOTE_PICTURE_MODE,
|
||||
"color_profile": const.REMOTE_COLOR_PROFILE,
|
||||
"lens_control": const.REMOTE_LENS_CONTROL,
|
||||
"setting_memory": const.REMOTE_SETTING_MEMORY,
|
||||
"gamma_settings": const.REMOTE_GAMMA_SETTINGS,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the JVC Projector platform from a config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([JvcProjectorRemote(coordinator)], True)
|
||||
|
||||
|
||||
class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity):
|
||||
"""Representation of a JVC Projector device."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self.coordinator.data["power"] in [const.ON, const.WARMING]
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
await self.device.power_on()
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
await self.device.power_off()
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
|
||||
"""Send a remote command to the device."""
|
||||
for cmd in command:
|
||||
if cmd not in COMMANDS:
|
||||
raise HomeAssistantError(f"{cmd} is not a known command")
|
||||
_LOGGER.debug("Sending command '%s'", cmd)
|
||||
await self.device.remote(COMMANDS[cmd])
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "IP address or hostname of projector",
|
||||
"port": "IP port of projector (default is 20554)",
|
||||
"password": "Optional password if projector is configured for one"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "Password authentication failed",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "Password authentication failed"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -214,6 +214,7 @@ FLOWS = {
|
|||
"jellyfin",
|
||||
"juicenet",
|
||||
"justnimbus",
|
||||
"jvc_projector",
|
||||
"kaleidescape",
|
||||
"keenetic_ndms2",
|
||||
"kegtron",
|
||||
|
|
|
@ -2658,6 +2658,12 @@
|
|||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"jvc_projector": {
|
||||
"name": "JVC Projector",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"kaiterra": {
|
||||
"name": "Kaiterra",
|
||||
"integration_type": "hub",
|
||||
|
|
10
mypy.ini
10
mypy.ini
|
@ -1542,6 +1542,16 @@ disallow_untyped_defs = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.jvc_projector.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.kaleidescape.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -1709,6 +1709,9 @@ pyisy==3.1.14
|
|||
# homeassistant.components.itach
|
||||
pyitachip2ir==0.0.7
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==1.0.6
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.0.1
|
||||
|
||||
|
|
|
@ -1243,6 +1243,9 @@ pyiss==1.0.1
|
|||
# homeassistant.components.isy994
|
||||
pyisy==3.1.14
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==1.0.6
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.0.1
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
"""Tests for JVC Projector integration."""
|
||||
|
||||
MOCK_HOST = "127.0.0.1"
|
||||
MOCK_PORT = 20554
|
||||
MOCK_PASSWORD = "jvcpasswd"
|
||||
MOCK_MAC = "jvcmac"
|
||||
MOCK_MODEL = "jvcmodel"
|
|
@ -0,0 +1,58 @@
|
|||
"""Fixtures for JVC Projector integration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.jvc_projector.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MOCK_HOST, MOCK_MAC, MOCK_MODEL, MOCK_PASSWORD, MOCK_PORT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_device")
|
||||
def fixture_mock_device(request) -> Generator[None, AsyncMock, None]:
|
||||
"""Return a mocked JVC Projector device."""
|
||||
target = "homeassistant.components.jvc_projector.JvcProjector"
|
||||
if hasattr(request, "param"):
|
||||
target = request.param
|
||||
|
||||
with patch(target, autospec=True) as mock:
|
||||
device = mock.return_value
|
||||
device.host = MOCK_HOST
|
||||
device.port = MOCK_PORT
|
||||
device.mac = MOCK_MAC
|
||||
device.model = MOCK_MODEL
|
||||
device.get_state.return_value = {"power": "standby"}
|
||||
yield device
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_config_entry")
|
||||
def fixture_mock_config_entry() -> MockConfigEntry:
|
||||
"""Return a mock config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=MOCK_MAC,
|
||||
version=1,
|
||||
data={
|
||||
CONF_HOST: MOCK_HOST,
|
||||
CONF_PORT: MOCK_PORT,
|
||||
CONF_PASSWORD: MOCK_PASSWORD,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_integration")
|
||||
async def fixture_mock_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> MockConfigEntry:
|
||||
"""Return a mock ConfigEntry setup for the integration."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return mock_config_entry
|
|
@ -0,0 +1,297 @@
|
|||
"""Tests for JVC Projector config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from jvcprojector import JvcProjectorAuthError, JvcProjectorConnectError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.jvc_projector.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import MOCK_HOST, MOCK_PASSWORD, MOCK_PORT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TARGET = "homeassistant.components.jvc_projector.config_flow.JvcProjector"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [TARGET], indirect=True)
|
||||
async def test_user_config_flow_success(
|
||||
hass: HomeAssistant, mock_device: AsyncMock
|
||||
) -> None:
|
||||
"""Test user config flow success."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: MOCK_HOST,
|
||||
CONF_PORT: MOCK_PORT,
|
||||
CONF_PASSWORD: MOCK_PASSWORD,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert "data" in result
|
||||
assert result["data"][CONF_HOST] == MOCK_HOST
|
||||
assert result["data"][CONF_PORT] == MOCK_PORT
|
||||
assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [TARGET], indirect=True)
|
||||
async def test_user_config_flow_bad_connect_errors(
|
||||
hass: HomeAssistant, mock_device: AsyncMock
|
||||
) -> None:
|
||||
"""Test errors when connection error occurs."""
|
||||
mock_device.connect.side_effect = JvcProjectorConnectError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
# Finish flow with success
|
||||
|
||||
mock_device.connect.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert "data" in result
|
||||
assert result["data"][CONF_HOST] == MOCK_HOST
|
||||
assert result["data"][CONF_PORT] == MOCK_PORT
|
||||
assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [TARGET], indirect=True)
|
||||
async def test_user_config_flow_device_exists_abort(
|
||||
hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test flow aborts when device already configured."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD},
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [TARGET], indirect=True)
|
||||
async def test_user_config_flow_bad_host_errors(
|
||||
hass: HomeAssistant, mock_device: AsyncMock
|
||||
) -> None:
|
||||
"""Test errors when bad host error occurs."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: "", CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "invalid_host"}
|
||||
|
||||
# Finish flow with success
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert "data" in result
|
||||
assert result["data"][CONF_HOST] == MOCK_HOST
|
||||
assert result["data"][CONF_PORT] == MOCK_PORT
|
||||
assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [TARGET], indirect=True)
|
||||
async def test_user_config_flow_bad_auth_errors(
|
||||
hass: HomeAssistant, mock_device: AsyncMock
|
||||
) -> None:
|
||||
"""Test errors when bad auth error occurs."""
|
||||
mock_device.connect.side_effect = JvcProjectorAuthError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
# Finish flow with success
|
||||
|
||||
mock_device.connect.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert "data" in result
|
||||
assert result["data"][CONF_HOST] == MOCK_HOST
|
||||
assert result["data"][CONF_PORT] == MOCK_PORT
|
||||
assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [TARGET], indirect=True)
|
||||
async def test_reauth_config_flow_success(
|
||||
hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test reauth config flow success."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": mock_integration.entry_id,
|
||||
},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PASSWORD: MOCK_PASSWORD}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
assert mock_integration.data[CONF_HOST] == MOCK_HOST
|
||||
assert mock_integration.data[CONF_PORT] == MOCK_PORT
|
||||
assert mock_integration.data[CONF_PASSWORD] == MOCK_PASSWORD
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [TARGET], indirect=True)
|
||||
async def test_reauth_config_flow_auth_error(
|
||||
hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test reauth config flow when connect fails."""
|
||||
mock_device.connect.side_effect = JvcProjectorAuthError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": mock_integration.entry_id,
|
||||
},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PASSWORD: MOCK_PASSWORD}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
# Finish flow with success
|
||||
|
||||
mock_device.connect.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": mock_integration.entry_id,
|
||||
},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PASSWORD: MOCK_PASSWORD}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
assert mock_integration.data[CONF_HOST] == MOCK_HOST
|
||||
assert mock_integration.data[CONF_PORT] == MOCK_PORT
|
||||
assert mock_integration.data[CONF_PASSWORD] == MOCK_PASSWORD
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", [TARGET], indirect=True)
|
||||
async def test_reauth_config_flow_connect_error(
|
||||
hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test reauth config flow when connect fails."""
|
||||
mock_device.connect.side_effect = JvcProjectorConnectError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": mock_integration.entry_id,
|
||||
},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PASSWORD: MOCK_PASSWORD}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
# Finish flow with success
|
||||
|
||||
mock_device.connect.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": mock_integration.entry_id,
|
||||
},
|
||||
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PASSWORD: MOCK_PASSWORD}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
assert mock_integration.data[CONF_HOST] == MOCK_HOST
|
||||
assert mock_integration.data[CONF_PORT] == MOCK_PORT
|
||||
assert mock_integration.data[CONF_PASSWORD] == MOCK_PASSWORD
|
|
@ -0,0 +1,73 @@
|
|||
"""Tests for JVC Projector config entry."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from jvcprojector import JvcProjectorAuthError, JvcProjectorConnectError
|
||||
|
||||
from homeassistant.components.jvc_projector import DOMAIN
|
||||
from homeassistant.components.jvc_projector.coordinator import (
|
||||
INTERVAL_FAST,
|
||||
INTERVAL_SLOW,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_coordinator_update(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test coordinator update runs."""
|
||||
mock_device.get_state.return_value = {"power": "standby"}
|
||||
async_fire_time_changed(
|
||||
hass, utcnow() + timedelta(seconds=INTERVAL_SLOW.seconds + 1)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_device.get_state.call_count == 3
|
||||
coordinator = hass.data[DOMAIN][mock_integration.entry_id]
|
||||
assert coordinator.update_interval == INTERVAL_SLOW
|
||||
|
||||
|
||||
async def test_coordinator_connect_error(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test coordinator connect error."""
|
||||
mock_device.get_state.side_effect = JvcProjectorConnectError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_coordinator_auth_error(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test coordinator auth error."""
|
||||
mock_device.get_state.side_effect = JvcProjectorAuthError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_coordinator_device_on(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test coordinator changes update interval when device is on."""
|
||||
mock_device.get_state.return_value = {"power": "on"}
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
coordinator = hass.data[DOMAIN][mock_config_entry.entry_id]
|
||||
assert coordinator.update_interval == INTERVAL_FAST
|
|
@ -0,0 +1,71 @@
|
|||
"""Tests for JVC Projector config entry."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from jvcprojector import JvcProjectorAuthError, JvcProjectorConnectError
|
||||
|
||||
from homeassistant.components.jvc_projector.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import MOCK_MAC
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_init(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test initialization."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_MAC)})
|
||||
assert device is not None
|
||||
assert device.identifiers == {(DOMAIN, MOCK_MAC)}
|
||||
|
||||
|
||||
async def test_unload_config_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config entry loading and unloading."""
|
||||
mock_config_entry = mock_integration
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.entry_id not in hass.data[DOMAIN]
|
||||
|
||||
|
||||
async def test_config_entry_connect_error(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config entry with connect error."""
|
||||
mock_device.connect.side_effect = JvcProjectorConnectError
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_auth_error(
|
||||
hass: HomeAssistant,
|
||||
mock_device: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config entry with auth error."""
|
||||
mock_device.connect.side_effect = JvcProjectorAuthError
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
|
@ -0,0 +1,77 @@
|
|||
"""Tests for JVC Projector remote platform."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.remote import (
|
||||
ATTR_COMMAND,
|
||||
DOMAIN as REMOTE_DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ENTITY_ID = "remote.jvc_projector"
|
||||
|
||||
|
||||
async def test_entity_state(
|
||||
hass: HomeAssistant,
|
||||
mock_device: MagicMock,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Tests entity state is registered."""
|
||||
entity = hass.states.get(ENTITY_ID)
|
||||
assert entity
|
||||
assert er.async_get(hass).async_get(entity.entity_id)
|
||||
|
||||
|
||||
async def test_commands(
|
||||
hass: HomeAssistant,
|
||||
mock_device: MagicMock,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test service call are called."""
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_device.power_on.call_count == 1
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_device.power_off.call_count == 1
|
||||
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["ok"]},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_device.remote.call_count == 1
|
||||
|
||||
|
||||
async def test_unknown_command(
|
||||
hass: HomeAssistant,
|
||||
mock_device: MagicMock,
|
||||
mock_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test unknown service call errors."""
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["bad"]},
|
||||
blocking=True,
|
||||
)
|
||||
assert str(err.value) == "bad is not a known command"
|
Loading…
Reference in New Issue