Add Switcher config flow discovery support (#52316)

This commit is contained in:
Shay Levy 2021-07-19 16:28:40 +03:00 committed by GitHub
parent 51d16202ab
commit ea6e325762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1146 additions and 582 deletions

View File

@ -992,8 +992,6 @@ omit =
homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swiss_public_transport/sensor.py
homeassistant/components/swisscom/device_tracker.py homeassistant/components/swisscom/device_tracker.py
homeassistant/components/switchbot/switch.py homeassistant/components/switchbot/switch.py
homeassistant/components/switcher_kis/sensor.py
homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/switch.py homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/__init__.py
homeassistant/components/syncthing/sensor.py homeassistant/components/syncthing/sensor.py

View File

@ -79,6 +79,7 @@ homeassistant.components.ssdp.*
homeassistant.components.stream.* homeassistant.components.stream.*
homeassistant.components.sun.* homeassistant.components.sun.*
homeassistant.components.switch.* homeassistant.components.switch.*
homeassistant.components.switcher_kis.*
homeassistant.components.synology_dsm.* homeassistant.components.synology_dsm.*
homeassistant.components.systemmonitor.* homeassistant.components.systemmonitor.*
homeassistant.components.tag.* homeassistant.components.tag.*

View File

@ -1,32 +1,40 @@
"""The Switcher integration.""" """The Switcher integration."""
from __future__ import annotations from __future__ import annotations
from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for from datetime import timedelta
from datetime import datetime, timedelta
import logging import logging
from aioswitcher.bridge import SwitcherV2Bridge from aioswitcher.device import SwitcherBase
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import (
from homeassistant.helpers.discovery import async_load_platform config_validation as cv,
device_registry,
update_coordinator,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import EventType
from .const import ( from .const import (
CONF_DEVICE_PASSWORD, CONF_DEVICE_PASSWORD,
CONF_PHONE_ID, CONF_PHONE_ID,
DATA_DEVICE, DATA_DEVICE,
DATA_DISCOVERY,
DOMAIN, DOMAIN,
SIGNAL_SWITCHER_DEVICE_UPDATE, MAX_UPDATE_INTERVAL_SEC,
SIGNAL_DEVICE_ADD,
) )
from .utils import async_start_bridge, async_stop_bridge
PLATFORMS = ["switch", "sensor"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema( CCONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{ {
@ -36,50 +44,138 @@ CONFIG_SCHEMA = vol.Schema(
} }
) )
}, },
),
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the switcher component.""" """Set up the switcher component."""
phone_id = config[DOMAIN][CONF_PHONE_ID] hass.data.setdefault(DOMAIN, {})
device_id = config[DOMAIN][CONF_DEVICE_ID]
device_password = config[DOMAIN][CONF_DEVICE_PASSWORD]
v2bridge = SwitcherV2Bridge(hass.loop, phone_id, device_id, device_password) if DOMAIN not in config:
return True
await v2bridge.start() hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
)
)
return True
async def async_stop_bridge(event: EventType) -> None:
"""On Home Assistant stop, gracefully stop the bridge if running."""
await v2bridge.stop()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_bridge) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Switcher from a config entry."""
try: hass.data[DOMAIN][DATA_DEVICE] = {}
device_data = await wait_for(v2bridge.queue.get(), timeout=10.0)
except (Asyncio_TimeoutError, RuntimeError):
_LOGGER.exception("Failed to get response from device")
await v2bridge.stop()
return False
hass.data[DOMAIN] = {DATA_DEVICE: device_data}
hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config))
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
@callback @callback
def device_updates(timestamp: datetime | None) -> None: def on_device_data_callback(device: SwitcherBase) -> None:
"""Use for updating the device data from the queue.""" """Use as a callback for device data."""
if v2bridge.running:
try:
device_new_data = v2bridge.queue.get_nowait()
if device_new_data:
async_dispatcher_send(
hass, SIGNAL_SWITCHER_DEVICE_UPDATE, device_new_data
)
except QueueEmpty:
pass
async_track_time_interval(hass, device_updates, timedelta(seconds=4)) # Existing device update device data
if device.device_id in hass.data[DOMAIN][DATA_DEVICE]:
wrapper: SwitcherDeviceWrapper = hass.data[DOMAIN][DATA_DEVICE][
device.device_id
]
wrapper.async_set_updated_data(device)
return
# New device - create device
_LOGGER.info(
"Discovered Switcher device - id: %s, name: %s, type: %s (%s)",
device.device_id,
device.name,
device.device_type.value,
device.device_type.hex_rep,
)
wrapper = hass.data[DOMAIN][DATA_DEVICE][
device.device_id
] = SwitcherDeviceWrapper(hass, entry, device)
hass.async_create_task(wrapper.async_setup())
async def platforms_setup_task() -> None:
# Must be ready before dispatcher is called
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_setup(entry, platform)
discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None)
if discovery_task is not None:
discovered_devices = await discovery_task
for device in discovered_devices.values():
on_device_data_callback(device)
await async_start_bridge(hass, on_device_data_callback)
hass.async_create_task(platforms_setup_task())
@callback
async def stop_bridge(event: Event) -> None:
await async_stop_bridge(hass)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge)
return True return True
class SwitcherDeviceWrapper(update_coordinator.DataUpdateCoordinator):
"""Wrapper for a Switcher device with Home Assistant specific functions."""
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, device: SwitcherBase
) -> None:
"""Initialize the Switcher device wrapper."""
super().__init__(
hass,
_LOGGER,
name=device.name,
update_interval=timedelta(seconds=MAX_UPDATE_INTERVAL_SEC),
)
self.hass = hass
self.entry = entry
self.data = device
async def _async_update_data(self) -> None:
"""Mark device offline if no data."""
raise update_coordinator.UpdateFailed(
f"Device {self.name} did not send update for {MAX_UPDATE_INTERVAL_SEC} seconds"
)
@property
def model(self) -> str:
"""Switcher device model."""
return self.data.device_type.value # type: ignore[no-any-return]
@property
def device_id(self) -> str:
"""Switcher device id."""
return self.data.device_id # type: ignore[no-any-return]
@property
def mac_address(self) -> str:
"""Switcher device mac address."""
return self.data.mac_address # type: ignore[no-any-return]
async def async_setup(self) -> None:
"""Set up the wrapper."""
dev_reg = await device_registry.async_get_registry(self.hass)
dev_reg.async_get_or_create(
config_entry_id=self.entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac_address)},
identifiers={(DOMAIN, self.device_id)},
manufacturer="Switcher",
name=self.name,
model=self.model,
)
async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADD, self)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
await async_stop_bridge(hass)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(DATA_DEVICE)
return unload_ok

View File

@ -0,0 +1,49 @@
"""Config flow for Switcher integration."""
from __future__ import annotations
from typing import Any
from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.typing import ConfigType
from .const import DATA_DISCOVERY, DOMAIN
from .utils import async_discover_devices
class SwitcherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle Switcher config flow."""
async def async_step_import(self, import_config: ConfigType) -> FlowResult:
"""Handle a flow initiated by import."""
if self._async_current_entries(True):
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="Switcher", data={})
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the start of the config flow."""
if self._async_current_entries(True):
return self.async_abort(reason="single_instance_allowed")
self.hass.data.setdefault(DOMAIN, {})
if DATA_DISCOVERY not in self.hass.data[DOMAIN]:
self.hass.data[DOMAIN][DATA_DISCOVERY] = self.hass.async_create_task(
async_discover_devices()
)
return self.async_show_form(step_id="confirm")
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle user-confirmation of the config flow."""
discovered_devices = await self.hass.data[DOMAIN][DATA_DISCOVERY]
if len(discovered_devices) == 0:
self.hass.data[DOMAIN].pop(DATA_DISCOVERY)
return self.async_abort(reason="no_devices_found")
return self.async_create_entry(title="Switcher", data={})

View File

@ -1,20 +1,22 @@
"""Constants for the Switcher integration.""" """Constants for the Switcher integration."""
DOMAIN = "switcher_kis" DOMAIN = "switcher_kis"
CONF_DEVICE_PASSWORD = "device_password" CONF_DEVICE_PASSWORD = "device_password"
CONF_PHONE_ID = "phone_id" CONF_PHONE_ID = "phone_id"
DATA_BRIDGE = "bridge"
DATA_DEVICE = "device" DATA_DEVICE = "device"
DATA_DISCOVERY = "discovery"
SIGNAL_SWITCHER_DEVICE_UPDATE = "switcher_device_update" DISCOVERY_TIME_SEC = 6
ATTR_AUTO_OFF_SET = "auto_off_set" SIGNAL_DEVICE_ADD = "switcher_device_add"
ATTR_ELECTRIC_CURRENT = "electric_current"
ATTR_REMAINING_TIME = "remaining_time"
# Services
CONF_AUTO_OFF = "auto_off" CONF_AUTO_OFF = "auto_off"
CONF_TIMER_MINUTES = "timer_minutes" CONF_TIMER_MINUTES = "timer_minutes"
SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" SERVICE_SET_AUTO_OFF_NAME = "set_auto_off"
SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer"
# Defines the maximum interval device must send an update before it marked unavailable
MAX_UPDATE_INTERVAL_SEC = 20

View File

@ -3,6 +3,7 @@
"name": "Switcher", "name": "Switcher",
"documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/",
"codeowners": ["@tomerfi","@thecode"], "codeowners": ["@tomerfi","@thecode"],
"requirements": ["aioswitcher==1.2.3"], "requirements": ["aioswitcher==2.0.4"],
"iot_class": "local_push" "iot_class": "local_push",
"config_flow": true
} }

View File

@ -3,8 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from aioswitcher.consts import WAITING_TEXT from aioswitcher.device import DeviceCategory
from aioswitcher.devices import SwitcherV2Device
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
DEVICE_CLASS_CURRENT, DEVICE_CLASS_CURRENT,
@ -12,13 +11,17 @@ from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
SensorEntity, SensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import DiscoveryInfoType, StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DATA_DEVICE, DOMAIN, SIGNAL_SWITCHER_DEVICE_UPDATE from . import SwitcherDeviceWrapper
from .const import SIGNAL_DEVICE_ADD
@dataclass @dataclass
@ -31,7 +34,6 @@ class AttributeDescription:
device_class: str | None = None device_class: str | None = None
state_class: str | None = None state_class: str | None = None
default_enabled: bool = True default_enabled: bool = True
default_value: float | int | str | None = None
POWER_SENSORS = { POWER_SENSORS = {
@ -40,14 +42,12 @@ POWER_SENSORS = {
unit=POWER_WATT, unit=POWER_WATT,
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
default_value=0,
), ),
"electric_current": AttributeDescription( "electric_current": AttributeDescription(
name="Electric Current", name="Electric Current",
unit=ELECTRICAL_CURRENT_AMPERE, unit=ELECTRICAL_CURRENT_AMPERE,
device_class=DEVICE_CLASS_CURRENT, device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
default_value=0.0,
), ),
} }
@ -55,77 +55,73 @@ TIME_SENSORS = {
"remaining_time": AttributeDescription( "remaining_time": AttributeDescription(
name="Remaining Time", name="Remaining Time",
icon="mdi:av-timer", icon="mdi:av-timer",
default_value="00:00:00",
), ),
"auto_off_set": AttributeDescription( "auto_off_set": AttributeDescription(
name="Auto Shutdown", name="Auto Shutdown",
icon="mdi:progress-clock", icon="mdi:progress-clock",
default_enabled=False, default_enabled=False,
default_value="00:00:00",
), ),
} }
SENSORS = {**POWER_SENSORS, **TIME_SENSORS} POWER_PLUG_SENSORS = POWER_SENSORS
WATER_HEATER_SENSORS = {**POWER_SENSORS, **TIME_SENSORS}
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: dict, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType,
) -> None: ) -> None:
"""Set up Switcher sensor from config entry.""" """Set up Switcher sensor from config entry."""
device_data = hass.data[DOMAIN][DATA_DEVICE]
@callback
def async_add_sensors(wrapper: SwitcherDeviceWrapper) -> None:
"""Add sensors from Switcher device."""
if wrapper.data.device_type.category == DeviceCategory.POWER_PLUG:
async_add_entities( async_add_entities(
SwitcherSensorEntity(device_data, attribute, sensor) SwitcherSensorEntity(wrapper, attribute, info)
for attribute, sensor in SENSORS.items() for attribute, info in POWER_PLUG_SENSORS.items()
)
elif wrapper.data.device_type.category == DeviceCategory.WATER_HEATER:
async_add_entities(
SwitcherSensorEntity(wrapper, attribute, info)
for attribute, info in WATER_HEATER_SENSORS.items()
)
config_entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_sensors)
) )
class SwitcherSensorEntity(SensorEntity): class SwitcherSensorEntity(CoordinatorEntity, SensorEntity):
"""Representation of a Switcher sensor entity.""" """Representation of a Switcher sensor entity."""
def __init__( def __init__(
self, self,
device_data: SwitcherV2Device, wrapper: SwitcherDeviceWrapper,
attribute: str, attribute: str,
description: AttributeDescription, description: AttributeDescription,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self._device_data = device_data super().__init__(wrapper)
self.wrapper = wrapper
self.attribute = attribute self.attribute = attribute
self.description = description
# Entity class attributes # Entity class attributes
self._attr_name = f"{self._device_data.name} {self.description.name}" self._attr_name = f"{wrapper.name} {description.name}"
self._attr_icon = self.description.icon self._attr_icon = description.icon
self._attr_unit_of_measurement = self.description.unit self._attr_unit_of_measurement = description.unit
self._attr_device_class = self.description.device_class self._attr_device_class = description.device_class
self._attr_entity_registry_enabled_default = self.description.default_enabled self._attr_entity_registry_enabled_default = description.default_enabled
self._attr_should_poll = False
self._attr_unique_id = f"{self._device_data.device_id}-{self._device_data.mac_addr}-{self.attribute}" self._attr_unique_id = f"{wrapper.device_id}-{wrapper.mac_address}-{attribute}"
self._attr_device_info = {
"connections": {
(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac_address)
}
}
@property @property
def state(self) -> StateType: def state(self) -> StateType:
"""Return value of sensor.""" """Return value of sensor."""
value = getattr(self._device_data, self.attribute) return getattr(self.wrapper.data, self.attribute) # type: ignore[no-any-return]
if value and value is not WAITING_TEXT:
return value
return self.description.default_value
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data
)
)
@callback
def async_update_data(self, device_data: SwitcherV2Device) -> None:
"""Update the entity data."""
self._device_data = device_data
self.async_write_ha_state()

View File

@ -5,6 +5,7 @@ set_auto_off:
entity: entity:
integration: switcher_kis integration: switcher_kis
domain: switch domain: switch
device_class: switch
fields: fields:
auto_off: auto_off:
name: Auto off name: Auto off
@ -21,6 +22,7 @@ turn_on_with_timer:
entity: entity:
integration: switcher_kis integration: switcher_kis
domain: switch domain: switch
device_class: switch
fields: fields:
timer_minutes: timer_minutes:
name: Timer name: Timer

View File

@ -0,0 +1,13 @@
{
"config": {
"step": {
"confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
}
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}

View File

@ -1,33 +1,42 @@
"""Home Assistant Switcher Component Switch platform.""" """Switcher integration Switch platform."""
from __future__ import annotations from __future__ import annotations
from aioswitcher.api import SwitcherV2Api import asyncio
from aioswitcher.api.messages import SwitcherV2ControlResponseMSG from datetime import timedelta
from aioswitcher.consts import ( import logging
COMMAND_OFF, from typing import Any
COMMAND_ON,
STATE_OFF as SWITCHER_STATE_OFF, from aioswitcher.api import Command, SwitcherApi, SwitcherBaseResponse
STATE_ON as SWITCHER_STATE_ON, from aioswitcher.device import DeviceCategory, DeviceState
)
from aioswitcher.devices import SwitcherV2Device
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import (
from homeassistant.core import HomeAssistant, ServiceCall, callback DEVICE_CLASS_OUTLET,
from homeassistant.helpers import config_validation as cv, entity_platform DEVICE_CLASS_SWITCH,
SwitchEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
config_validation as cv,
device_registry,
entity_platform,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import SwitcherDeviceWrapper
from .const import ( from .const import (
CONF_AUTO_OFF, CONF_AUTO_OFF,
CONF_TIMER_MINUTES, CONF_TIMER_MINUTES,
DATA_DEVICE,
DOMAIN,
SERVICE_SET_AUTO_OFF_NAME, SERVICE_SET_AUTO_OFF_NAME,
SERVICE_TURN_ON_WITH_TIMER_NAME, SERVICE_TURN_ON_WITH_TIMER_NAME,
SIGNAL_SWITCHER_DEVICE_UPDATE, SIGNAL_DEVICE_ADD,
) )
_LOGGER = logging.getLogger(__name__)
SERVICE_SET_AUTO_OFF_SCHEMA = { SERVICE_SET_AUTO_OFF_SCHEMA = {
vol.Required(CONF_AUTO_OFF): cv.time_period_str, vol.Required(CONF_AUTO_OFF): cv.time_period_str,
} }
@ -39,135 +48,142 @@ SERVICE_TURN_ON_WITH_TIMER_SCHEMA = {
} }
async def async_setup_platform( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: dict, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: dict,
) -> None: ) -> None:
"""Set up the switcher platform for the switch component.""" """Set up Switcher switch from config entry."""
if discovery_info is None:
return
async def async_set_auto_off_service(entity, service_call: ServiceCall) -> None:
"""Use for handling setting device auto-off service calls."""
async with SwitcherV2Api(
hass.loop,
device_data.ip_addr,
device_data.phone_id,
device_data.device_id,
device_data.device_password,
) as swapi:
await swapi.set_auto_shutdown(service_call.data[CONF_AUTO_OFF])
async def async_turn_on_with_timer_service(
entity, service_call: ServiceCall
) -> None:
"""Use for handling turning device on with a timer service calls."""
async with SwitcherV2Api(
hass.loop,
device_data.ip_addr,
device_data.phone_id,
device_data.device_id,
device_data.device_password,
) as swapi:
await swapi.control_device(
COMMAND_ON, service_call.data[CONF_TIMER_MINUTES]
)
device_data = hass.data[DOMAIN][DATA_DEVICE]
async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])])
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_AUTO_OFF_NAME, SERVICE_SET_AUTO_OFF_NAME,
SERVICE_SET_AUTO_OFF_SCHEMA, SERVICE_SET_AUTO_OFF_SCHEMA,
async_set_auto_off_service, "async_set_auto_off_service",
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_TURN_ON_WITH_TIMER_NAME, SERVICE_TURN_ON_WITH_TIMER_NAME,
SERVICE_TURN_ON_WITH_TIMER_SCHEMA, SERVICE_TURN_ON_WITH_TIMER_SCHEMA,
async_turn_on_with_timer_service, "async_turn_on_with_timer_service",
)
@callback
def async_add_switch(wrapper: SwitcherDeviceWrapper) -> None:
"""Add switch from Switcher device."""
if wrapper.data.device_type.category == DeviceCategory.POWER_PLUG:
async_add_entities([SwitcherPowerPlugSwitchEntity(wrapper)])
elif wrapper.data.device_type.category == DeviceCategory.WATER_HEATER:
async_add_entities([SwitcherWaterHeaterSwitchEntity(wrapper)])
config_entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_switch)
) )
class SwitcherControl(SwitchEntity): class SwitcherBaseSwitchEntity(CoordinatorEntity, SwitchEntity):
"""Home Assistant switch entity.""" """Representation of a Switcher switch entity."""
def __init__(self, device_data: SwitcherV2Device) -> None: def __init__(self, wrapper: SwitcherDeviceWrapper) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self._self_initiated = False super().__init__(wrapper)
self._device_data = device_data self.wrapper = wrapper
self._state = device_data.state self.control_result: bool | None = None
@property # Entity class attributes
def name(self) -> str: self._attr_name = wrapper.name
"""Return the device's name.""" self._attr_unique_id = f"{wrapper.device_id}-{wrapper.mac_address}"
return self._device_data.name self._attr_device_info = {
"connections": {
(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac_address)
}
}
@property @callback
def should_poll(self) -> bool: def _handle_coordinator_update(self) -> None:
"""Return False, entity pushes its state to HA.""" """When device updates, clear control result that overrides state."""
return False self.control_result = None
self.async_write_ha_state()
@property async def _async_call_api(self, api: str, *args: Any) -> None:
def unique_id(self) -> str: """Call Switcher API."""
"""Return a unique ID.""" _LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args)
return f"{self._device_data.device_id}-{self._device_data.mac_addr}" response: SwitcherBaseResponse = None
error = None
try:
async with SwitcherApi(
self.wrapper.data.ip_address, self.wrapper.device_id
) as swapi:
response = await getattr(swapi, api)(*args)
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
error = repr(err)
if error or not response or not response.successful:
_LOGGER.error(
"Call api for %s failed, api: '%s', args: %s, response/error: %s",
self.name,
api,
args,
response or error,
)
self.wrapper.last_update_success = False
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if entity is on.""" """Return True if entity is on."""
return self._state == SWITCHER_STATE_ON if self.control_result is not None:
return self.control_result
@property return bool(self.wrapper.data.device_state == DeviceState.ON)
def available(self) -> bool:
"""Return True if entity is available."""
return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF]
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data
)
)
@callback
def async_update_data(self, device_data: SwitcherV2Device) -> None:
"""Update the entity data."""
if self._self_initiated:
self._self_initiated = False
else:
self._device_data = device_data
self._state = self._device_data.state
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: dict) -> None: async def async_turn_on(self, **kwargs: dict) -> None:
"""Turn the entity on.""" """Turn the entity on."""
await self._control_device(True) await self._async_call_api("control_device", Command.ON)
self.control_result = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: dict) -> None: async def async_turn_off(self, **kwargs: dict) -> None:
"""Turn the entity off.""" """Turn the entity off."""
await self._control_device(False) await self._async_call_api("control_device", Command.OFF)
self.control_result = False
self.async_write_ha_state()
async def _control_device(self, send_on: bool) -> None: async def async_set_auto_off_service(self, auto_off: timedelta) -> None:
"""Turn the entity on or off.""" """Use for handling setting device auto-off service calls."""
response: SwitcherV2ControlResponseMSG = None _LOGGER.warning(
async with SwitcherV2Api( "Service '%s' is not supported by %s",
self.hass.loop, SERVICE_SET_AUTO_OFF_NAME,
self._device_data.ip_addr, self.name,
self._device_data.phone_id,
self._device_data.device_id,
self._device_data.device_password,
) as swapi:
response = await swapi.control_device(
COMMAND_ON if send_on else COMMAND_OFF
) )
if response and response.successful: async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None:
self._self_initiated = True """Use for turning device on with a timer service calls."""
self._state = SWITCHER_STATE_ON if send_on else SWITCHER_STATE_OFF _LOGGER.warning(
"Service '%s' is not supported by %s",
SERVICE_TURN_ON_WITH_TIMER_NAME,
self.name,
)
class SwitcherPowerPlugSwitchEntity(SwitcherBaseSwitchEntity):
"""Representation of a Switcher power plug switch entity."""
_attr_device_class = DEVICE_CLASS_OUTLET
class SwitcherWaterHeaterSwitchEntity(SwitcherBaseSwitchEntity):
"""Representation of a Switcher water heater switch entity."""
_attr_device_class = DEVICE_CLASS_SWITCH
async def async_set_auto_off_service(self, auto_off: timedelta) -> None:
"""Use for handling setting device auto-off service calls."""
await self._async_call_api("set_auto_shutdown", auto_off)
self.async_write_ha_state()
async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None:
"""Use for turning device on with a timer service calls."""
await self._async_call_api("control_device", Command.ON, timer_minutes)
self.control_result = True
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -0,0 +1,13 @@
{
"config": {
"abort": {
"no_devices_found": "No devices found on the network",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"step": {
"confirm": {
"description": "Do you want to start set up?"
}
}
}
}

View File

@ -0,0 +1,54 @@
"""Switcher integration helpers functions."""
from __future__ import annotations
import asyncio
import logging
from typing import Any, Callable
from aioswitcher.bridge import SwitcherBase, SwitcherBridge
from homeassistant.core import HomeAssistant, callback
from .const import DATA_BRIDGE, DISCOVERY_TIME_SEC, DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_start_bridge(
hass: HomeAssistant, on_device_callback: Callable[[SwitcherBase], Any]
) -> None:
"""Start switcher UDP bridge."""
bridge = hass.data[DOMAIN][DATA_BRIDGE] = SwitcherBridge(on_device_callback)
_LOGGER.debug("Starting Switcher bridge")
await bridge.start()
async def async_stop_bridge(hass: HomeAssistant) -> None:
"""Stop switcher UDP bridge."""
bridge: SwitcherBridge = hass.data[DOMAIN].get(DATA_BRIDGE)
if bridge is not None:
_LOGGER.debug("Stopping Switcher bridge")
await bridge.stop()
hass.data[DOMAIN].pop(DATA_BRIDGE)
async def async_discover_devices() -> dict[str, SwitcherBase]:
"""Discover Switcher devices."""
_LOGGER.debug("Starting discovery")
discovered_devices = {}
@callback
def on_device_data_callback(device: SwitcherBase) -> None:
"""Use as a callback for device data."""
if device.device_id in discovered_devices:
return
discovered_devices[device.device_id] = device
bridge = SwitcherBridge(on_device_data_callback)
await bridge.start()
await asyncio.sleep(DISCOVERY_TIME_SEC)
await bridge.stop()
_LOGGER.debug("Finished discovery, discovered devices: %s", len(discovered_devices))
return discovered_devices

View File

@ -251,6 +251,7 @@ FLOWS = [
"srp_energy", "srp_energy",
"starline", "starline",
"subaru", "subaru",
"switcher_kis",
"syncthing", "syncthing",
"syncthru", "syncthru",
"synology_dsm", "synology_dsm",

View File

@ -880,6 +880,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.switcher_kis.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.synology_dsm.*] [mypy-homeassistant.components.synology_dsm.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
@ -1539,9 +1550,6 @@ ignore_errors = true
[mypy-homeassistant.components.switchbot.*] [mypy-homeassistant.components.switchbot.*]
ignore_errors = true ignore_errors = true
[mypy-homeassistant.components.switcher_kis.*]
ignore_errors = true
[mypy-homeassistant.components.synology_srm.*] [mypy-homeassistant.components.synology_srm.*]
ignore_errors = true ignore_errors = true

View File

@ -236,7 +236,7 @@ aiorecollect==1.0.5
aioshelly==0.6.4 aioshelly==0.6.4
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==1.2.3 aioswitcher==2.0.4
# homeassistant.components.syncthing # homeassistant.components.syncthing
aiosyncthing==0.5.1 aiosyncthing==0.5.1

View File

@ -158,7 +158,7 @@ aiorecollect==1.0.5
aioshelly==0.6.4 aioshelly==0.6.4
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==1.2.3 aioswitcher==2.0.4
# homeassistant.components.syncthing # homeassistant.components.syncthing
aiosyncthing==0.5.1 aiosyncthing==0.5.1

View File

@ -175,7 +175,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.stt.*", "homeassistant.components.stt.*",
"homeassistant.components.surepetcare.*", "homeassistant.components.surepetcare.*",
"homeassistant.components.switchbot.*", "homeassistant.components.switchbot.*",
"homeassistant.components.switcher_kis.*",
"homeassistant.components.synology_srm.*", "homeassistant.components.synology_srm.*",
"homeassistant.components.system_health.*", "homeassistant.components.system_health.*",
"homeassistant.components.system_log.*", "homeassistant.components.system_log.*",

View File

@ -1 +1,16 @@
"""Test cases and object for the Switcher integration tests.""" """Test cases and object for the Switcher integration tests."""
from homeassistant.components.switcher_kis.const import DOMAIN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Set up the Switcher integration in Home Assistant."""
entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry

View File

@ -1,194 +1,61 @@
"""Common fixtures and objects for the Switcher integration tests.""" """Common fixtures and objects for the Switcher integration tests."""
from __future__ import annotations from unittest.mock import AsyncMock, Mock, patch
from asyncio import Queue import pytest
from datetime import datetime
from typing import Any, Generator
from unittest.mock import AsyncMock, patch
from pytest import fixture
from .consts import (
DUMMY_AUTO_OFF_SET,
DUMMY_DEVICE_ID,
DUMMY_DEVICE_NAME,
DUMMY_DEVICE_PASSWORD,
DUMMY_DEVICE_STATE,
DUMMY_ELECTRIC_CURRENT,
DUMMY_IP_ADDRESS,
DUMMY_MAC_ADDRESS,
DUMMY_PHONE_ID,
DUMMY_POWER_CONSUMPTION,
DUMMY_REMAINING_TIME,
)
@patch("aioswitcher.devices.SwitcherV2Device") @pytest.fixture
class MockSwitcherV2Device: def mock_bridge(request):
"""Class for mocking the aioswitcher.devices.SwitcherV2Device object.""" """Return a mocked SwitcherBridge."""
with patch(
"homeassistant.components.switcher_kis.utils.SwitcherBridge", autospec=True
) as bridge_mock:
bridge = bridge_mock.return_value
def __init__(self) -> None: bridge.devices = []
"""Initialize the object.""" if hasattr(request, "param") and request.param:
self._last_state_change = datetime.now() bridge.devices = request.param
@property async def start():
def device_id(self) -> str: bridge.is_running = True
"""Return the device id."""
return DUMMY_DEVICE_ID
@property for device in bridge.devices:
def ip_addr(self) -> str: bridge_mock.call_args[0][0](device)
"""Return the ip address."""
return DUMMY_IP_ADDRESS
@property def mock_callbacks(devices):
def mac_addr(self) -> str: for device in devices:
"""Return the mac address.""" bridge_mock.call_args[0][0](device)
return DUMMY_MAC_ADDRESS
@property async def stop():
def name(self) -> str: bridge.is_running = False
"""Return the device name."""
return DUMMY_DEVICE_NAME
@property bridge.start = AsyncMock(side_effect=start)
def state(self) -> str: bridge.mock_callbacks = Mock(side_effect=mock_callbacks)
"""Return the device state.""" bridge.stop = AsyncMock(side_effect=stop)
return DUMMY_DEVICE_STATE
@property yield bridge
def remaining_time(self) -> str | None:
"""Return the time left to auto-off."""
return DUMMY_REMAINING_TIME
@property
def auto_off_set(self) -> str:
"""Return the auto-off configuration value."""
return DUMMY_AUTO_OFF_SET
@property
def power_consumption(self) -> int:
"""Return the power consumption in watts."""
return DUMMY_POWER_CONSUMPTION
@property
def electric_current(self) -> float:
"""Return the power consumption in amps."""
return DUMMY_ELECTRIC_CURRENT
@property
def phone_id(self) -> str:
"""Return the phone id."""
return DUMMY_PHONE_ID
@property
def device_password(self) -> str:
"""Return the device password."""
return DUMMY_DEVICE_PASSWORD
@property
def last_data_update(self) -> datetime:
"""Return the timestamp of the last update."""
return datetime.now()
@property
def last_state_change(self) -> datetime:
"""Return the timestamp of the state change."""
return self._last_state_change
@fixture(name="mock_bridge") @pytest.fixture
def mock_bridge_fixture() -> Generator[None, Any, None]: def mock_api():
"""Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge.""" """Fixture for mocking aioswitcher.api.SwitcherApi."""
queue = Queue() api_mock = AsyncMock()
async def mock_queue():
"""Mock asyncio's Queue."""
await queue.put(MockSwitcherV2Device())
return await queue.get()
mock_bridge = AsyncMock()
patchers = [ patchers = [
patch( patch(
"homeassistant.components.switcher_kis.SwitcherV2Bridge.start", "homeassistant.components.switcher_kis.switch.SwitcherApi.connect",
new=mock_bridge, new=api_mock,
), ),
patch( patch(
"homeassistant.components.switcher_kis.SwitcherV2Bridge.stop", "homeassistant.components.switcher_kis.switch.SwitcherApi.disconnect",
new=mock_bridge, new=api_mock,
),
patch(
"homeassistant.components.switcher_kis.SwitcherV2Bridge.queue",
get=mock_queue,
),
patch(
"homeassistant.components.switcher_kis.SwitcherV2Bridge.running",
return_value=True,
), ),
] ]
for patcher in patchers: for patcher in patchers:
patcher.start() patcher.start()
yield yield api_mock
for patcher in patchers:
patcher.stop()
@fixture(name="mock_failed_bridge")
def mock_failed_bridge_fixture() -> Generator[None, Any, None]:
"""Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge."""
async def mock_queue():
"""Mock asyncio's Queue."""
raise RuntimeError
patchers = [
patch(
"homeassistant.components.switcher_kis.SwitcherV2Bridge.start",
return_value=None,
),
patch(
"homeassistant.components.switcher_kis.SwitcherV2Bridge.stop",
return_value=None,
),
patch(
"homeassistant.components.switcher_kis.SwitcherV2Bridge.queue",
get=mock_queue,
),
]
for patcher in patchers:
patcher.start()
yield
for patcher in patchers:
patcher.stop()
@fixture(name="mock_api")
def mock_api_fixture() -> Generator[AsyncMock, Any, None]:
"""Fixture for mocking aioswitcher.api.SwitcherV2Api."""
mock_api = AsyncMock()
patchers = [
patch(
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.connect",
new=mock_api,
),
patch(
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.disconnect",
new=mock_api,
),
]
for patcher in patchers:
patcher.start()
yield
for patcher in patchers: for patcher in patchers:
patcher.stop() patcher.stop()

View File

@ -1,5 +1,12 @@
"""Constants for the Switcher integration tests.""" """Constants for the Switcher integration tests."""
from aioswitcher.device import (
DeviceState,
DeviceType,
SwitcherPowerPlug,
SwitcherWaterHeater,
)
from homeassistant.components.switcher_kis import ( from homeassistant.components.switcher_kis import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DEVICE_PASSWORD, CONF_DEVICE_PASSWORD,
@ -8,27 +15,54 @@ from homeassistant.components.switcher_kis import (
) )
DUMMY_AUTO_OFF_SET = "01:30:00" DUMMY_AUTO_OFF_SET = "01:30:00"
DUMMY_TIMER_MINUTES_SET = "90" DUMMY_AUTO_SHUT_DOWN = "02:00:00"
DUMMY_DEVICE_ID = "a123bc" DUMMY_DEVICE_ID1 = "a123bc"
DUMMY_DEVICE_NAME = "Device Name" DUMMY_DEVICE_ID2 = "cafe12"
DUMMY_DEVICE_NAME1 = "Plug 23BC"
DUMMY_DEVICE_NAME2 = "Heater FE12"
DUMMY_DEVICE_PASSWORD = "12345678" DUMMY_DEVICE_PASSWORD = "12345678"
DUMMY_DEVICE_STATE = "on" DUMMY_ELECTRIC_CURRENT1 = 0.5
DUMMY_ELECTRIC_CURRENT = 12.8 DUMMY_ELECTRIC_CURRENT2 = 12.8
DUMMY_ICON = "mdi:dummy-icon" DUMMY_IP_ADDRESS1 = "192.168.100.157"
DUMMY_IP_ADDRESS = "192.168.100.157" DUMMY_IP_ADDRESS2 = "192.168.100.158"
DUMMY_MAC_ADDRESS = "A1:B2:C3:45:67:D8" DUMMY_MAC_ADDRESS1 = "A1:B2:C3:45:67:D8"
DUMMY_NAME = "boiler" DUMMY_MAC_ADDRESS2 = "A1:B2:C3:45:67:D9"
DUMMY_PHONE_ID = "1234" DUMMY_PHONE_ID = "1234"
DUMMY_POWER_CONSUMPTION = 2780 DUMMY_POWER_CONSUMPTION1 = 100
DUMMY_POWER_CONSUMPTION2 = 2780
DUMMY_REMAINING_TIME = "01:29:32" DUMMY_REMAINING_TIME = "01:29:32"
DUMMY_TIMER_MINUTES_SET = "90"
# Adjust if any modification were made to DUMMY_DEVICE_NAME YAML_CONFIG = {
SWITCH_ENTITY_ID = "switch.device_name"
MANDATORY_CONFIGURATION = {
DOMAIN: { DOMAIN: {
CONF_PHONE_ID: DUMMY_PHONE_ID, CONF_PHONE_ID: DUMMY_PHONE_ID,
CONF_DEVICE_ID: DUMMY_DEVICE_ID, CONF_DEVICE_ID: DUMMY_DEVICE_ID1,
CONF_DEVICE_PASSWORD: DUMMY_DEVICE_PASSWORD, CONF_DEVICE_PASSWORD: DUMMY_DEVICE_PASSWORD,
} }
} }
DUMMY_PLUG_DEVICE = SwitcherPowerPlug(
DeviceType.POWER_PLUG,
DeviceState.ON,
DUMMY_DEVICE_ID1,
DUMMY_IP_ADDRESS1,
DUMMY_MAC_ADDRESS1,
DUMMY_DEVICE_NAME1,
DUMMY_POWER_CONSUMPTION1,
DUMMY_ELECTRIC_CURRENT1,
)
DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater(
DeviceType.V4,
DeviceState.ON,
DUMMY_DEVICE_ID2,
DUMMY_IP_ADDRESS2,
DUMMY_MAC_ADDRESS2,
DUMMY_DEVICE_NAME2,
DUMMY_POWER_CONSUMPTION2,
DUMMY_ELECTRIC_CURRENT2,
DUMMY_REMAINING_TIME,
DUMMY_AUTO_SHUT_DOWN,
)
DUMMY_SWITCHER_DEVICES = [DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE]

View File

@ -0,0 +1,106 @@
"""Test the Switcher config flow."""
from unittest.mock import patch
import pytest
from homeassistant import config_entries
from homeassistant.components.switcher_kis.const import DATA_DISCOVERY, DOMAIN
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE
from tests.common import MockConfigEntry
async def test_import(hass):
"""Test import step."""
with patch(
"homeassistant.components.switcher_kis.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Switcher"
assert result["data"] == {}
@pytest.mark.parametrize(
"mock_bridge",
[
[
DUMMY_PLUG_DEVICE,
DUMMY_WATER_HEATER_DEVICE,
# Make sure we don't detect the same device twice
DUMMY_WATER_HEATER_DEVICE,
]
],
indirect=True,
)
async def test_user_setup(hass, mock_bridge):
"""Test we can finish a config flow."""
with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert mock_bridge.is_running is False
assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 2
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
assert result["errors"] is None
with patch(
"homeassistant.components.switcher_kis.async_setup_entry", return_value=True
):
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Switcher"
assert result2["result"].data == {}
async def test_user_setup_abort_no_devices_found(hass, mock_bridge):
"""Test we abort a config flow if no devices found."""
with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert mock_bridge.is_running is False
assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 0
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "confirm"
assert result["errors"] is None
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] == RESULT_TYPE_ABORT
assert result2["reason"] == "no_devices_found"
@pytest.mark.parametrize(
"source",
[
config_entries.SOURCE_IMPORT,
config_entries.SOURCE_USER,
],
)
async def test_single_instance(hass, source):
"""Test we only allow a single config flow."""
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": source}
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"

View File

@ -1,200 +1,111 @@
"""Test cases for the switcher_kis component.""" """Test cases for the switcher_kis component."""
from datetime import timedelta from datetime import timedelta
from typing import Any, Generator
from unittest.mock import patch from unittest.mock import patch
from aioswitcher.consts import COMMAND_ON import pytest
from aioswitcher.devices import SwitcherV2Device
from pytest import raises
from homeassistant.components.switcher_kis import ( from homeassistant import config_entries
from homeassistant.components.switcher_kis.const import (
DATA_DEVICE, DATA_DEVICE,
DOMAIN, DOMAIN,
SIGNAL_SWITCHER_DEVICE_UPDATE, MAX_UPDATE_INTERVAL_SEC,
) )
from homeassistant.components.switcher_kis.switch import ( from homeassistant.config_entries import ConfigEntryState
CONF_AUTO_OFF, from homeassistant.const import STATE_UNAVAILABLE
CONF_TIMER_MINUTES,
SERVICE_SET_AUTO_OFF_NAME,
SERVICE_TURN_ON_WITH_TIMER_NAME,
)
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.exceptions import UnknownUser
from homeassistant.helpers.config_validation import time_period_str
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt from homeassistant.util import dt, slugify
from .consts import ( from . import init_integration
DUMMY_AUTO_OFF_SET, from .consts import DUMMY_SWITCHER_DEVICES, YAML_CONFIG
DUMMY_DEVICE_ID,
DUMMY_DEVICE_NAME,
DUMMY_DEVICE_STATE,
DUMMY_ELECTRIC_CURRENT,
DUMMY_IP_ADDRESS,
DUMMY_MAC_ADDRESS,
DUMMY_PHONE_ID,
DUMMY_POWER_CONSUMPTION,
DUMMY_REMAINING_TIME,
DUMMY_TIMER_MINUTES_SET,
MANDATORY_CONFIGURATION,
SWITCH_ENTITY_ID,
)
from tests.common import MockUser, async_fire_time_changed from tests.common import async_fire_time_changed
async def test_failed_config( @pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
hass: HomeAssistant, mock_failed_bridge: Generator[None, Any, None] async def test_async_setup_yaml_config(hass, mock_bridge) -> None:
) -> None: """Test setup started by configuration from YAML."""
"""Test failed configuration.""" assert await async_setup_component(hass, DOMAIN, YAML_CONFIG)
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) is False
async def test_minimal_config(
hass: HomeAssistant, mock_bridge: Generator[None, Any, None]
) -> None:
"""Test setup with configuration minimal entries."""
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
async def test_discovery_data_bucket(
hass: HomeAssistant, mock_bridge: Generator[None, Any, None]
) -> None:
"""Test the event send with the updated device."""
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
await hass.async_block_till_done() await hass.async_block_till_done()
device = hass.data[DOMAIN].get(DATA_DEVICE) assert mock_bridge.is_running is True
assert device.device_id == DUMMY_DEVICE_ID assert len(hass.data[DOMAIN]) == 2
assert device.ip_addr == DUMMY_IP_ADDRESS assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
assert device.mac_addr == DUMMY_MAC_ADDRESS
assert device.name == DUMMY_DEVICE_NAME
assert device.state == DUMMY_DEVICE_STATE
assert device.remaining_time == DUMMY_REMAINING_TIME
assert device.auto_off_set == DUMMY_AUTO_OFF_SET
assert device.power_consumption == DUMMY_POWER_CONSUMPTION
assert device.electric_current == DUMMY_ELECTRIC_CURRENT
assert device.phone_id == DUMMY_PHONE_ID
async def test_set_auto_off_service( @pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
hass: HomeAssistant, async def test_async_setup_user_config_flow(hass, mock_bridge) -> None:
mock_bridge: Generator[None, Any, None], """Test setup started by user config flow."""
mock_api: Generator[None, Any, None], with patch("homeassistant.components.switcher_kis.utils.asyncio.sleep"):
hass_owner_user: MockUser, result = await hass.config_entries.flow.async_init(
) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}
"""Test the set_auto_off service.""" )
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) await hass.config_entries.flow.async_configure(result["flow_id"], {})
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.services.has_service(DOMAIN, SERVICE_SET_AUTO_OFF_NAME) assert mock_bridge.is_running is True
assert len(hass.data[DOMAIN]) == 2
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
await hass.services.async_call(
DOMAIN,
SERVICE_SET_AUTO_OFF_NAME,
{CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
blocking=True,
context=Context(user_id=hass_owner_user.id),
)
with raises(UnknownUser) as unknown_user_exc: async def test_update_fail(hass, mock_bridge, caplog):
await hass.services.async_call( """Test entities state unavailable when updates fail.."""
DOMAIN, await init_integration(hass)
SERVICE_SET_AUTO_OFF_NAME, assert mock_bridge
{CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
blocking=True,
context=Context(user_id="not_real_user"),
)
assert unknown_user_exc.type is UnknownUser
with patch(
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.set_auto_shutdown"
) as mock_set_auto_shutdown:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_AUTO_OFF_NAME,
{CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
)
mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_set_auto_shutdown.assert_called_once_with( assert mock_bridge.is_running is True
time_period_str(DUMMY_AUTO_OFF_SET) assert len(hass.data[DOMAIN]) == 2
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
async_fire_time_changed(
hass, dt.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC + 1)
) )
async def test_turn_on_with_timer_service(
hass: HomeAssistant,
mock_bridge: Generator[None, Any, None],
mock_api: Generator[None, Any, None],
hass_owner_user: MockUser,
) -> None:
"""Test the set_auto_off service."""
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.services.has_service(DOMAIN, SERVICE_TURN_ON_WITH_TIMER_NAME) for device in DUMMY_SWITCHER_DEVICES:
assert (
await hass.services.async_call( f"Device {device.name} did not send update for {MAX_UPDATE_INTERVAL_SEC} seconds"
DOMAIN, in caplog.text
SERVICE_TURN_ON_WITH_TIMER_NAME,
{CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET},
blocking=True,
context=Context(user_id=hass_owner_user.id),
) )
with raises(UnknownUser) as unknown_user_exc: entity_id = f"switch.{slugify(device.name)}"
await hass.services.async_call( state = hass.states.get(entity_id)
DOMAIN, assert state.state == STATE_UNAVAILABLE
SERVICE_TURN_ON_WITH_TIMER_NAME,
{ entity_id = f"sensor.{slugify(device.name)}_power_consumption"
CONF_ENTITY_ID: SWITCH_ENTITY_ID, state = hass.states.get(entity_id)
CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, assert state.state == STATE_UNAVAILABLE
},
blocking=True, mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES)
context=Context(user_id="not_real_user"), await hass.async_block_till_done()
async_fire_time_changed(
hass, dt.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC - 1)
) )
assert unknown_user_exc.type is UnknownUser for device in DUMMY_SWITCHER_DEVICES:
entity_id = f"switch.{slugify(device.name)}"
state = hass.states.get(entity_id)
assert state.state != STATE_UNAVAILABLE
with patch( entity_id = f"sensor.{slugify(device.name)}_power_consumption"
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.control_device" state = hass.states.get(entity_id)
) as mock_control_device: assert state.state != STATE_UNAVAILABLE
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON_WITH_TIMER_NAME,
{
CONF_ENTITY_ID: SWITCH_ENTITY_ID,
CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET,
},
)
async def test_entry_unload(hass, mock_bridge):
"""Test entry unload."""
entry = await init_integration(hass)
assert mock_bridge
assert entry.state is ConfigEntryState.LOADED
assert mock_bridge.is_running is True
assert len(hass.data[DOMAIN]) == 2
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_control_device.assert_called_once_with( assert entry.state is ConfigEntryState.NOT_LOADED
COMMAND_ON, int(DUMMY_TIMER_MINUTES_SET) assert mock_bridge.is_running is False
) assert len(hass.data[DOMAIN]) == 0
async def test_signal_dispatcher(
hass: HomeAssistant, mock_bridge: Generator[None, Any, None]
) -> None:
"""Test signal dispatcher dispatching device updates every 4 seconds."""
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
await hass.async_block_till_done()
@callback
def verify_update_data(device: SwitcherV2Device) -> None:
"""Use as callback for signal dispatcher."""
pass
async_dispatcher_connect(hass, SIGNAL_SWITCHER_DEVICE_UPDATE, verify_update_data)
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=5))

View File

@ -0,0 +1,93 @@
"""Test the Switcher Sensor Platform."""
import pytest
from homeassistant.components.switcher_kis.const import DATA_DEVICE, DOMAIN
from homeassistant.helpers import entity_registry as er
from homeassistant.util import slugify
from . import init_integration
from .consts import DUMMY_PLUG_DEVICE, DUMMY_SWITCHER_DEVICES, DUMMY_WATER_HEATER_DEVICE
DEVICE_SENSORS_TUPLE = (
(
DUMMY_PLUG_DEVICE,
[
"power_consumption",
"electric_current",
],
),
(
DUMMY_WATER_HEATER_DEVICE,
[
"power_consumption",
"electric_current",
"remaining_time",
],
),
)
@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
async def test_sensor_platform(hass, mock_bridge):
"""Test sensor platform."""
await init_integration(hass)
assert mock_bridge
assert mock_bridge.is_running is True
assert len(hass.data[DOMAIN]) == 2
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
for device, sensors in DEVICE_SENSORS_TUPLE:
for sensor in sensors:
entity_id = f"sensor.{slugify(device.name)}_{sensor}"
state = hass.states.get(entity_id)
assert state.state == str(getattr(device, sensor))
async def test_sensor_disabled(hass, mock_bridge):
"""Test sensor disabled by default."""
await init_integration(hass)
assert mock_bridge
mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE])
await hass.async_block_till_done()
registry = er.async_get(hass)
device = DUMMY_WATER_HEATER_DEVICE
unique_id = f"{device.device_id}-{device.mac_address}-auto_off_set"
entity_id = f"sensor.{slugify(device.name)}_auto_shutdown"
entry = registry.async_get(entity_id)
assert entry
assert entry.unique_id == unique_id
assert entry.disabled is True
assert entry.disabled_by == er.DISABLED_INTEGRATION
# Test enabling entity
updated_entry = registry.async_update_entity(
entry.entity_id, **{"disabled_by": None}
)
assert updated_entry != entry
assert updated_entry.disabled is False
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
async def test_sensor_update(hass, mock_bridge, monkeypatch):
"""Test sensor update."""
await init_integration(hass)
assert mock_bridge
device = DUMMY_WATER_HEATER_DEVICE
sensor = "power_consumption"
entity_id = f"sensor.{slugify(device.name)}_{sensor}"
state = hass.states.get(entity_id)
assert state.state == str(getattr(device, sensor))
monkeypatch.setattr(device, sensor, 1431)
mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE])
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "1431"

View File

@ -0,0 +1,162 @@
"""Test the services for the Switcher integration."""
from unittest.mock import patch
from aioswitcher.api import Command
from aioswitcher.device import DeviceState
import pytest
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.switcher_kis.const import (
CONF_AUTO_OFF,
CONF_TIMER_MINUTES,
DOMAIN,
SERVICE_SET_AUTO_OFF_NAME,
SERVICE_TURN_ON_WITH_TIMER_NAME,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.helpers.config_validation import time_period_str
from homeassistant.util import slugify
from . import init_integration
from .consts import (
DUMMY_AUTO_OFF_SET,
DUMMY_PLUG_DEVICE,
DUMMY_TIMER_MINUTES_SET,
DUMMY_WATER_HEATER_DEVICE,
)
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
async def test_turn_on_with_timer_service(hass, mock_bridge, mock_api, monkeypatch):
"""Test the turn on with timer service."""
await init_integration(hass)
assert mock_bridge
device = DUMMY_WATER_HEATER_DEVICE
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
# Test initial state - off
monkeypatch.setattr(device, "device_state", DeviceState.OFF)
mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE])
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
with patch(
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device"
) as mock_control_device:
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON_WITH_TIMER_NAME,
{
ATTR_ENTITY_ID: entity_id,
CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET,
},
blocking=True,
)
assert mock_api.call_count == 2
mock_control_device.assert_called_once_with(
Command.ON, int(DUMMY_TIMER_MINUTES_SET)
)
state = hass.states.get(entity_id)
assert state.state == STATE_ON
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
async def test_set_auto_off_service(hass, mock_bridge, mock_api):
"""Test the set auto off service."""
await init_integration(hass)
assert mock_bridge
device = DUMMY_WATER_HEATER_DEVICE
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
with patch(
"homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown"
) as mock_set_auto_shutdown:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_AUTO_OFF_NAME,
{ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
blocking=True,
)
assert mock_api.call_count == 2
mock_set_auto_shutdown.assert_called_once_with(
time_period_str(DUMMY_AUTO_OFF_SET)
)
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
async def test_set_auto_off_service_fail(hass, mock_bridge, mock_api, caplog):
"""Test set auto off service failed."""
await init_integration(hass)
assert mock_bridge
device = DUMMY_WATER_HEATER_DEVICE
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
with patch(
"homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown",
return_value=None,
) as mock_set_auto_shutdown:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_AUTO_OFF_NAME,
{ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
blocking=True,
)
assert mock_api.call_count == 2
mock_set_auto_shutdown.assert_called_once_with(
time_period_str(DUMMY_AUTO_OFF_SET)
)
assert (
f"Call api for {device.name} failed, api: 'set_auto_shutdown'"
in caplog.text
)
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize("mock_bridge", [[DUMMY_PLUG_DEVICE]], indirect=True)
async def test_plug_unsupported_services(hass, mock_bridge, mock_api, caplog):
"""Test plug device unsupported services."""
await init_integration(hass)
assert mock_bridge
device = DUMMY_PLUG_DEVICE
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
# Turn on with timer
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON_WITH_TIMER_NAME,
{
ATTR_ENTITY_ID: entity_id,
CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET,
},
blocking=True,
)
assert mock_api.call_count == 0
assert (
f"Service '{SERVICE_TURN_ON_WITH_TIMER_NAME}' is not supported by {device.name}"
in caplog.text
)
# Auto off
await hass.services.async_call(
DOMAIN,
SERVICE_SET_AUTO_OFF_NAME,
{ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
blocking=True,
)
assert mock_api.call_count == 0
assert (
f"Service '{SERVICE_SET_AUTO_OFF_NAME}' is not supported by {device.name}"
in caplog.text
)

View File

@ -0,0 +1,127 @@
"""Test the Switcher switch platform."""
from unittest.mock import patch
from aioswitcher.api import Command, SwitcherBaseResponse
from aioswitcher.device import DeviceState
import pytest
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.util import slugify
from . import init_integration
from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE
@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True)
async def test_switch(hass, mock_bridge, mock_api, monkeypatch):
"""Test the switch."""
await init_integration(hass)
assert mock_bridge
device = DUMMY_WATER_HEATER_DEVICE
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
# Test initial state - on
state = hass.states.get(entity_id)
assert state.state == STATE_ON
# Test state change on --> off
monkeypatch.setattr(device, "device_state", DeviceState.OFF)
mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE])
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
# Test turning on
with patch(
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device",
) as mock_control_device:
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert mock_api.call_count == 2
mock_control_device.assert_called_once_with(Command.ON)
state = hass.states.get(entity_id)
assert state.state == STATE_ON
# Test turning off
with patch(
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device"
) as mock_control_device:
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert mock_api.call_count == 4
mock_control_device.assert_called_once_with(Command.OFF)
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
@pytest.mark.parametrize("mock_bridge", [[DUMMY_PLUG_DEVICE]], indirect=True)
async def test_switch_control_fail(hass, mock_bridge, mock_api, monkeypatch, caplog):
"""Test switch control fail."""
await init_integration(hass)
assert mock_bridge
device = DUMMY_PLUG_DEVICE
entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}"
# Test initial state - off
monkeypatch.setattr(device, "device_state", DeviceState.OFF)
mock_bridge.mock_callbacks([DUMMY_PLUG_DEVICE])
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
# Test exception during turn on
with patch(
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device",
side_effect=RuntimeError("fake error"),
) as mock_control_device:
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert mock_api.call_count == 2
mock_control_device.assert_called_once_with(Command.ON)
assert (
f"Call api for {device.name} failed, api: 'control_device'" in caplog.text
)
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE
# Make device available again
mock_bridge.mock_callbacks([DUMMY_PLUG_DEVICE])
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
# Test error response during turn on
with patch(
"homeassistant.components.switcher_kis.switch.SwitcherApi.control_device",
return_value=SwitcherBaseResponse(None),
) as mock_control_device:
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert mock_api.call_count == 4
mock_control_device.assert_called_once_with(Command.ON)
assert (
f"Call api for {device.name} failed, api: 'control_device'" in caplog.text
)
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE