mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add Switcher config flow discovery support (#52316)
This commit is contained in:
parent
51d16202ab
commit
ea6e325762
@ -992,8 +992,6 @@ omit =
|
||||
homeassistant/components/swiss_public_transport/sensor.py
|
||||
homeassistant/components/swisscom/device_tracker.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/syncthing/__init__.py
|
||||
homeassistant/components/syncthing/sensor.py
|
||||
|
@ -79,6 +79,7 @@ homeassistant.components.ssdp.*
|
||||
homeassistant.components.stream.*
|
||||
homeassistant.components.sun.*
|
||||
homeassistant.components.switch.*
|
||||
homeassistant.components.switcher_kis.*
|
||||
homeassistant.components.synology_dsm.*
|
||||
homeassistant.components.systemmonitor.*
|
||||
homeassistant.components.tag.*
|
||||
|
@ -1,85 +1,181 @@
|
||||
"""The Switcher integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aioswitcher.bridge import SwitcherV2Bridge
|
||||
from aioswitcher.device import SwitcherBase
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
update_coordinator,
|
||||
)
|
||||
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 (
|
||||
CONF_DEVICE_PASSWORD,
|
||||
CONF_PHONE_ID,
|
||||
DATA_DEVICE,
|
||||
DATA_DISCOVERY,
|
||||
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__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PHONE_ID): cv.string,
|
||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||
vol.Required(CONF_DEVICE_PASSWORD): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
CCONFIG_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
cv.deprecated(DOMAIN),
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PHONE_ID): cv.string,
|
||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||
vol.Required(CONF_DEVICE_PASSWORD): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the switcher component."""
|
||||
phone_id = config[DOMAIN][CONF_PHONE_ID]
|
||||
device_id = config[DOMAIN][CONF_DEVICE_ID]
|
||||
device_password = config[DOMAIN][CONF_DEVICE_PASSWORD]
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
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))
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Switcher from a config entry."""
|
||||
hass.data[DOMAIN][DATA_DEVICE] = {}
|
||||
|
||||
@callback
|
||||
def device_updates(timestamp: datetime | None) -> None:
|
||||
"""Use for updating the device data from the queue."""
|
||||
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
|
||||
def on_device_data_callback(device: SwitcherBase) -> None:
|
||||
"""Use as a callback for device data."""
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
49
homeassistant/components/switcher_kis/config_flow.py
Normal file
49
homeassistant/components/switcher_kis/config_flow.py
Normal 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={})
|
@ -1,20 +1,22 @@
|
||||
"""Constants for the Switcher integration."""
|
||||
|
||||
DOMAIN = "switcher_kis"
|
||||
|
||||
CONF_DEVICE_PASSWORD = "device_password"
|
||||
CONF_PHONE_ID = "phone_id"
|
||||
|
||||
DATA_BRIDGE = "bridge"
|
||||
DATA_DEVICE = "device"
|
||||
DATA_DISCOVERY = "discovery"
|
||||
|
||||
SIGNAL_SWITCHER_DEVICE_UPDATE = "switcher_device_update"
|
||||
DISCOVERY_TIME_SEC = 6
|
||||
|
||||
ATTR_AUTO_OFF_SET = "auto_off_set"
|
||||
ATTR_ELECTRIC_CURRENT = "electric_current"
|
||||
ATTR_REMAINING_TIME = "remaining_time"
|
||||
SIGNAL_DEVICE_ADD = "switcher_device_add"
|
||||
|
||||
# Services
|
||||
CONF_AUTO_OFF = "auto_off"
|
||||
CONF_TIMER_MINUTES = "timer_minutes"
|
||||
|
||||
SERVICE_SET_AUTO_OFF_NAME = "set_auto_off"
|
||||
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
|
||||
|
@ -3,6 +3,7 @@
|
||||
"name": "Switcher",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switcher_kis/",
|
||||
"codeowners": ["@tomerfi","@thecode"],
|
||||
"requirements": ["aioswitcher==1.2.3"],
|
||||
"iot_class": "local_push"
|
||||
"requirements": ["aioswitcher==2.0.4"],
|
||||
"iot_class": "local_push",
|
||||
"config_flow": true
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aioswitcher.consts import WAITING_TEXT
|
||||
from aioswitcher.devices import SwitcherV2Device
|
||||
from aioswitcher.device import DeviceCategory
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASS_CURRENT,
|
||||
@ -12,13 +11,17 @@ from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
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
|
||||
@ -31,7 +34,6 @@ class AttributeDescription:
|
||||
device_class: str | None = None
|
||||
state_class: str | None = None
|
||||
default_enabled: bool = True
|
||||
default_value: float | int | str | None = None
|
||||
|
||||
|
||||
POWER_SENSORS = {
|
||||
@ -40,14 +42,12 @@ POWER_SENSORS = {
|
||||
unit=POWER_WATT,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
default_value=0,
|
||||
),
|
||||
"electric_current": AttributeDescription(
|
||||
name="Electric Current",
|
||||
unit=ELECTRICAL_CURRENT_AMPERE,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
default_value=0.0,
|
||||
),
|
||||
}
|
||||
|
||||
@ -55,77 +55,73 @@ TIME_SENSORS = {
|
||||
"remaining_time": AttributeDescription(
|
||||
name="Remaining Time",
|
||||
icon="mdi:av-timer",
|
||||
default_value="00:00:00",
|
||||
),
|
||||
"auto_off_set": AttributeDescription(
|
||||
name="Auto Shutdown",
|
||||
icon="mdi:progress-clock",
|
||||
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,
|
||||
config: dict,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType,
|
||||
) -> None:
|
||||
"""Set up Switcher sensor from config entry."""
|
||||
device_data = hass.data[DOMAIN][DATA_DEVICE]
|
||||
|
||||
async_add_entities(
|
||||
SwitcherSensorEntity(device_data, attribute, sensor)
|
||||
for attribute, sensor in SENSORS.items()
|
||||
@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(
|
||||
SwitcherSensorEntity(wrapper, attribute, info)
|
||||
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."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_data: SwitcherV2Device,
|
||||
wrapper: SwitcherDeviceWrapper,
|
||||
attribute: str,
|
||||
description: AttributeDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._device_data = device_data
|
||||
super().__init__(wrapper)
|
||||
self.wrapper = wrapper
|
||||
self.attribute = attribute
|
||||
self.description = description
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_name = f"{self._device_data.name} {self.description.name}"
|
||||
self._attr_icon = self.description.icon
|
||||
self._attr_unit_of_measurement = self.description.unit
|
||||
self._attr_device_class = self.description.device_class
|
||||
self._attr_entity_registry_enabled_default = self.description.default_enabled
|
||||
self._attr_should_poll = False
|
||||
self._attr_name = f"{wrapper.name} {description.name}"
|
||||
self._attr_icon = description.icon
|
||||
self._attr_unit_of_measurement = description.unit
|
||||
self._attr_device_class = description.device_class
|
||||
self._attr_entity_registry_enabled_default = description.default_enabled
|
||||
|
||||
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
|
||||
def state(self) -> StateType:
|
||||
"""Return value of sensor."""
|
||||
value = getattr(self._device_data, self.attribute)
|
||||
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()
|
||||
return getattr(self.wrapper.data, self.attribute) # type: ignore[no-any-return]
|
||||
|
@ -5,6 +5,7 @@ set_auto_off:
|
||||
entity:
|
||||
integration: switcher_kis
|
||||
domain: switch
|
||||
device_class: switch
|
||||
fields:
|
||||
auto_off:
|
||||
name: Auto off
|
||||
@ -21,6 +22,7 @@ turn_on_with_timer:
|
||||
entity:
|
||||
integration: switcher_kis
|
||||
domain: switch
|
||||
device_class: switch
|
||||
fields:
|
||||
timer_minutes:
|
||||
name: Timer
|
||||
|
13
homeassistant/components/switcher_kis/strings.json
Normal file
13
homeassistant/components/switcher_kis/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +1,42 @@
|
||||
"""Home Assistant Switcher Component Switch platform."""
|
||||
"""Switcher integration Switch platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aioswitcher.api import SwitcherV2Api
|
||||
from aioswitcher.api.messages import SwitcherV2ControlResponseMSG
|
||||
from aioswitcher.consts import (
|
||||
COMMAND_OFF,
|
||||
COMMAND_ON,
|
||||
STATE_OFF as SWITCHER_STATE_OFF,
|
||||
STATE_ON as SWITCHER_STATE_ON,
|
||||
)
|
||||
from aioswitcher.devices import SwitcherV2Device
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioswitcher.api import Command, SwitcherApi, SwitcherBaseResponse
|
||||
from aioswitcher.device import DeviceCategory, DeviceState
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.components.switch import (
|
||||
DEVICE_CLASS_OUTLET,
|
||||
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.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import SwitcherDeviceWrapper
|
||||
from .const import (
|
||||
CONF_AUTO_OFF,
|
||||
CONF_TIMER_MINUTES,
|
||||
DATA_DEVICE,
|
||||
DOMAIN,
|
||||
SERVICE_SET_AUTO_OFF_NAME,
|
||||
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
||||
SIGNAL_SWITCHER_DEVICE_UPDATE,
|
||||
SIGNAL_DEVICE_ADD,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_SET_AUTO_OFF_SCHEMA = {
|
||||
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,
|
||||
config: dict,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: dict,
|
||||
) -> None:
|
||||
"""Set up the switcher platform for the switch component."""
|
||||
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])])
|
||||
|
||||
"""Set up Switcher switch from config entry."""
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_AUTO_OFF_NAME,
|
||||
SERVICE_SET_AUTO_OFF_SCHEMA,
|
||||
async_set_auto_off_service,
|
||||
"async_set_auto_off_service",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_TURN_ON_WITH_TIMER_NAME,
|
||||
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):
|
||||
"""Home Assistant switch entity."""
|
||||
class SwitcherBaseSwitchEntity(CoordinatorEntity, SwitchEntity):
|
||||
"""Representation of a Switcher switch entity."""
|
||||
|
||||
def __init__(self, device_data: SwitcherV2Device) -> None:
|
||||
def __init__(self, wrapper: SwitcherDeviceWrapper) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._self_initiated = False
|
||||
self._device_data = device_data
|
||||
self._state = device_data.state
|
||||
super().__init__(wrapper)
|
||||
self.wrapper = wrapper
|
||||
self.control_result: bool | None = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the device's name."""
|
||||
return self._device_data.name
|
||||
# Entity class attributes
|
||||
self._attr_name = wrapper.name
|
||||
self._attr_unique_id = f"{wrapper.device_id}-{wrapper.mac_address}"
|
||||
self._attr_device_info = {
|
||||
"connections": {
|
||||
(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac_address)
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return False, entity pushes its state to HA."""
|
||||
return False
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""When device updates, clear control result that overrides state."""
|
||||
self.control_result = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return f"{self._device_data.device_id}-{self._device_data.mac_addr}"
|
||||
async def _async_call_api(self, api: str, *args: Any) -> None:
|
||||
"""Call Switcher API."""
|
||||
_LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args)
|
||||
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
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self._state == SWITCHER_STATE_ON
|
||||
if self.control_result is not None:
|
||||
return self.control_result
|
||||
|
||||
@property
|
||||
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()
|
||||
return bool(self.wrapper.data.device_state == DeviceState.ON)
|
||||
|
||||
async def async_turn_on(self, **kwargs: dict) -> None:
|
||||
"""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:
|
||||
"""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:
|
||||
"""Turn the entity on or off."""
|
||||
response: SwitcherV2ControlResponseMSG = None
|
||||
async with SwitcherV2Api(
|
||||
self.hass.loop,
|
||||
self._device_data.ip_addr,
|
||||
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
|
||||
)
|
||||
async def async_set_auto_off_service(self, auto_off: timedelta) -> None:
|
||||
"""Use for handling setting device auto-off service calls."""
|
||||
_LOGGER.warning(
|
||||
"Service '%s' is not supported by %s",
|
||||
SERVICE_SET_AUTO_OFF_NAME,
|
||||
self.name,
|
||||
)
|
||||
|
||||
if response and response.successful:
|
||||
self._self_initiated = True
|
||||
self._state = SWITCHER_STATE_ON if send_on else SWITCHER_STATE_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."""
|
||||
_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()
|
||||
|
13
homeassistant/components/switcher_kis/translations/en.json
Normal file
13
homeassistant/components/switcher_kis/translations/en.json
Normal 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?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
homeassistant/components/switcher_kis/utils.py
Normal file
54
homeassistant/components/switcher_kis/utils.py
Normal 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
|
@ -251,6 +251,7 @@ FLOWS = [
|
||||
"srp_energy",
|
||||
"starline",
|
||||
"subaru",
|
||||
"switcher_kis",
|
||||
"syncthing",
|
||||
"syncthru",
|
||||
"synology_dsm",
|
||||
|
14
mypy.ini
14
mypy.ini
@ -880,6 +880,17 @@ no_implicit_optional = true
|
||||
warn_return_any = 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.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
@ -1539,9 +1550,6 @@ ignore_errors = true
|
||||
[mypy-homeassistant.components.switchbot.*]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.switcher_kis.*]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.synology_srm.*]
|
||||
ignore_errors = true
|
||||
|
||||
|
@ -236,7 +236,7 @@ aiorecollect==1.0.5
|
||||
aioshelly==0.6.4
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==1.2.3
|
||||
aioswitcher==2.0.4
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.5.1
|
||||
|
@ -158,7 +158,7 @@ aiorecollect==1.0.5
|
||||
aioshelly==0.6.4
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==1.2.3
|
||||
aioswitcher==2.0.4
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.5.1
|
||||
|
@ -175,7 +175,6 @@ IGNORED_MODULES: Final[list[str]] = [
|
||||
"homeassistant.components.stt.*",
|
||||
"homeassistant.components.surepetcare.*",
|
||||
"homeassistant.components.switchbot.*",
|
||||
"homeassistant.components.switcher_kis.*",
|
||||
"homeassistant.components.synology_srm.*",
|
||||
"homeassistant.components.system_health.*",
|
||||
"homeassistant.components.system_log.*",
|
||||
|
@ -1 +1,16 @@
|
||||
"""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
|
||||
|
@ -1,194 +1,61 @@
|
||||
"""Common fixtures and objects for the Switcher integration tests."""
|
||||
from __future__ import annotations
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from asyncio import Queue
|
||||
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,
|
||||
)
|
||||
import pytest
|
||||
|
||||
|
||||
@patch("aioswitcher.devices.SwitcherV2Device")
|
||||
class MockSwitcherV2Device:
|
||||
"""Class for mocking the aioswitcher.devices.SwitcherV2Device object."""
|
||||
@pytest.fixture
|
||||
def mock_bridge(request):
|
||||
"""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:
|
||||
"""Initialize the object."""
|
||||
self._last_state_change = datetime.now()
|
||||
bridge.devices = []
|
||||
if hasattr(request, "param") and request.param:
|
||||
bridge.devices = request.param
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
"""Return the device id."""
|
||||
return DUMMY_DEVICE_ID
|
||||
async def start():
|
||||
bridge.is_running = True
|
||||
|
||||
@property
|
||||
def ip_addr(self) -> str:
|
||||
"""Return the ip address."""
|
||||
return DUMMY_IP_ADDRESS
|
||||
for device in bridge.devices:
|
||||
bridge_mock.call_args[0][0](device)
|
||||
|
||||
@property
|
||||
def mac_addr(self) -> str:
|
||||
"""Return the mac address."""
|
||||
return DUMMY_MAC_ADDRESS
|
||||
def mock_callbacks(devices):
|
||||
for device in devices:
|
||||
bridge_mock.call_args[0][0](device)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the device name."""
|
||||
return DUMMY_DEVICE_NAME
|
||||
async def stop():
|
||||
bridge.is_running = False
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Return the device state."""
|
||||
return DUMMY_DEVICE_STATE
|
||||
bridge.start = AsyncMock(side_effect=start)
|
||||
bridge.mock_callbacks = Mock(side_effect=mock_callbacks)
|
||||
bridge.stop = AsyncMock(side_effect=stop)
|
||||
|
||||
@property
|
||||
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
|
||||
yield bridge
|
||||
|
||||
|
||||
@fixture(name="mock_bridge")
|
||||
def mock_bridge_fixture() -> Generator[None, Any, None]:
|
||||
"""Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge."""
|
||||
queue = Queue()
|
||||
|
||||
async def mock_queue():
|
||||
"""Mock asyncio's Queue."""
|
||||
await queue.put(MockSwitcherV2Device())
|
||||
return await queue.get()
|
||||
|
||||
mock_bridge = AsyncMock()
|
||||
@pytest.fixture
|
||||
def mock_api():
|
||||
"""Fixture for mocking aioswitcher.api.SwitcherApi."""
|
||||
api_mock = AsyncMock()
|
||||
|
||||
patchers = [
|
||||
patch(
|
||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.start",
|
||||
new=mock_bridge,
|
||||
"homeassistant.components.switcher_kis.switch.SwitcherApi.connect",
|
||||
new=api_mock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.stop",
|
||||
new=mock_bridge,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.queue",
|
||||
get=mock_queue,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switcher_kis.SwitcherV2Bridge.running",
|
||||
return_value=True,
|
||||
"homeassistant.components.switcher_kis.switch.SwitcherApi.disconnect",
|
||||
new=api_mock,
|
||||
),
|
||||
]
|
||||
|
||||
for patcher in patchers:
|
||||
patcher.start()
|
||||
|
||||
yield
|
||||
|
||||
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
|
||||
yield api_mock
|
||||
|
||||
for patcher in patchers:
|
||||
patcher.stop()
|
||||
|
@ -1,5 +1,12 @@
|
||||
"""Constants for the Switcher integration tests."""
|
||||
|
||||
from aioswitcher.device import (
|
||||
DeviceState,
|
||||
DeviceType,
|
||||
SwitcherPowerPlug,
|
||||
SwitcherWaterHeater,
|
||||
)
|
||||
|
||||
from homeassistant.components.switcher_kis import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DEVICE_PASSWORD,
|
||||
@ -8,27 +15,54 @@ from homeassistant.components.switcher_kis import (
|
||||
)
|
||||
|
||||
DUMMY_AUTO_OFF_SET = "01:30:00"
|
||||
DUMMY_TIMER_MINUTES_SET = "90"
|
||||
DUMMY_DEVICE_ID = "a123bc"
|
||||
DUMMY_DEVICE_NAME = "Device Name"
|
||||
DUMMY_AUTO_SHUT_DOWN = "02:00:00"
|
||||
DUMMY_DEVICE_ID1 = "a123bc"
|
||||
DUMMY_DEVICE_ID2 = "cafe12"
|
||||
DUMMY_DEVICE_NAME1 = "Plug 23BC"
|
||||
DUMMY_DEVICE_NAME2 = "Heater FE12"
|
||||
DUMMY_DEVICE_PASSWORD = "12345678"
|
||||
DUMMY_DEVICE_STATE = "on"
|
||||
DUMMY_ELECTRIC_CURRENT = 12.8
|
||||
DUMMY_ICON = "mdi:dummy-icon"
|
||||
DUMMY_IP_ADDRESS = "192.168.100.157"
|
||||
DUMMY_MAC_ADDRESS = "A1:B2:C3:45:67:D8"
|
||||
DUMMY_NAME = "boiler"
|
||||
DUMMY_ELECTRIC_CURRENT1 = 0.5
|
||||
DUMMY_ELECTRIC_CURRENT2 = 12.8
|
||||
DUMMY_IP_ADDRESS1 = "192.168.100.157"
|
||||
DUMMY_IP_ADDRESS2 = "192.168.100.158"
|
||||
DUMMY_MAC_ADDRESS1 = "A1:B2:C3:45:67:D8"
|
||||
DUMMY_MAC_ADDRESS2 = "A1:B2:C3:45:67:D9"
|
||||
DUMMY_PHONE_ID = "1234"
|
||||
DUMMY_POWER_CONSUMPTION = 2780
|
||||
DUMMY_POWER_CONSUMPTION1 = 100
|
||||
DUMMY_POWER_CONSUMPTION2 = 2780
|
||||
DUMMY_REMAINING_TIME = "01:29:32"
|
||||
DUMMY_TIMER_MINUTES_SET = "90"
|
||||
|
||||
# Adjust if any modification were made to DUMMY_DEVICE_NAME
|
||||
SWITCH_ENTITY_ID = "switch.device_name"
|
||||
|
||||
MANDATORY_CONFIGURATION = {
|
||||
YAML_CONFIG = {
|
||||
DOMAIN: {
|
||||
CONF_PHONE_ID: DUMMY_PHONE_ID,
|
||||
CONF_DEVICE_ID: DUMMY_DEVICE_ID,
|
||||
CONF_DEVICE_ID: DUMMY_DEVICE_ID1,
|
||||
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]
|
||||
|
106
tests/components/switcher_kis/test_config_flow.py
Normal file
106
tests/components/switcher_kis/test_config_flow.py
Normal 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"
|
@ -1,200 +1,111 @@
|
||||
"""Test cases for the switcher_kis component."""
|
||||
from datetime import timedelta
|
||||
from typing import Any, Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioswitcher.consts import COMMAND_ON
|
||||
from aioswitcher.devices import SwitcherV2Device
|
||||
from pytest import raises
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.switcher_kis import (
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.switcher_kis.const import (
|
||||
DATA_DEVICE,
|
||||
DOMAIN,
|
||||
SIGNAL_SWITCHER_DEVICE_UPDATE,
|
||||
MAX_UPDATE_INTERVAL_SEC,
|
||||
)
|
||||
from homeassistant.components.switcher_kis.switch import (
|
||||
CONF_AUTO_OFF,
|
||||
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.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.util import dt, slugify
|
||||
|
||||
from .consts import (
|
||||
DUMMY_AUTO_OFF_SET,
|
||||
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 . import init_integration
|
||||
from .consts import DUMMY_SWITCHER_DEVICES, YAML_CONFIG
|
||||
|
||||
from tests.common import MockUser, async_fire_time_changed
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def test_failed_config(
|
||||
hass: HomeAssistant, mock_failed_bridge: Generator[None, Any, None]
|
||||
) -> None:
|
||||
"""Test failed configuration."""
|
||||
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)
|
||||
|
||||
@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
|
||||
async def test_async_setup_yaml_config(hass, mock_bridge) -> None:
|
||||
"""Test setup started by configuration from YAML."""
|
||||
assert await async_setup_component(hass, DOMAIN, YAML_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = hass.data[DOMAIN].get(DATA_DEVICE)
|
||||
assert device.device_id == DUMMY_DEVICE_ID
|
||||
assert device.ip_addr == DUMMY_IP_ADDRESS
|
||||
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
|
||||
assert mock_bridge.is_running is True
|
||||
assert len(hass.data[DOMAIN]) == 2
|
||||
assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2
|
||||
|
||||
|
||||
async def test_set_auto_off_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)
|
||||
|
||||
@pytest.mark.parametrize("mock_bridge", [DUMMY_SWITCHER_DEVICES], indirect=True)
|
||||
async def test_async_setup_user_config_flow(hass, mock_bridge) -> None:
|
||||
"""Test setup started by user 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}
|
||||
)
|
||||
await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
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),
|
||||
|
||||
async def test_update_fail(hass, mock_bridge, caplog):
|
||||
"""Test entities state unavailable when updates fail.."""
|
||||
await init_integration(hass)
|
||||
assert mock_bridge
|
||||
|
||||
mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_bridge.is_running is True
|
||||
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)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for device in DUMMY_SWITCHER_DEVICES:
|
||||
assert (
|
||||
f"Device {device.name} did not send update for {MAX_UPDATE_INTERVAL_SEC} seconds"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
entity_id = f"switch.{slugify(device.name)}"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
entity_id = f"sensor.{slugify(device.name)}_power_consumption"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
mock_bridge.mock_callbacks(DUMMY_SWITCHER_DEVICES)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(
|
||||
hass, dt.utcnow() + timedelta(seconds=MAX_UPDATE_INTERVAL_SEC - 1)
|
||||
)
|
||||
|
||||
with raises(UnknownUser) as unknown_user_exc:
|
||||
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="not_real_user"),
|
||||
)
|
||||
for device in DUMMY_SWITCHER_DEVICES:
|
||||
entity_id = f"switch.{slugify(device.name)}"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
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},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_set_auto_shutdown.assert_called_once_with(
|
||||
time_period_str(DUMMY_AUTO_OFF_SET)
|
||||
)
|
||||
entity_id = f"sensor.{slugify(device.name)}_power_consumption"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
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)
|
||||
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()
|
||||
|
||||
assert hass.services.has_service(DOMAIN, SERVICE_TURN_ON_WITH_TIMER_NAME)
|
||||
|
||||
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},
|
||||
blocking=True,
|
||||
context=Context(user_id=hass_owner_user.id),
|
||||
)
|
||||
|
||||
with raises(UnknownUser) as unknown_user_exc:
|
||||
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,
|
||||
},
|
||||
blocking=True,
|
||||
context=Context(user_id="not_real_user"),
|
||||
)
|
||||
|
||||
assert unknown_user_exc.type is UnknownUser
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switcher_kis.switch.SwitcherV2Api.control_device"
|
||||
) as mock_control_device:
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_control_device.assert_called_once_with(
|
||||
COMMAND_ON, int(DUMMY_TIMER_MINUTES_SET)
|
||||
)
|
||||
|
||||
|
||||
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))
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert mock_bridge.is_running is False
|
||||
assert len(hass.data[DOMAIN]) == 0
|
||||
|
93
tests/components/switcher_kis/test_sensor.py
Normal file
93
tests/components/switcher_kis/test_sensor.py
Normal 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"
|
162
tests/components/switcher_kis/test_services.py
Normal file
162
tests/components/switcher_kis/test_services.py
Normal 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
|
||||
)
|
127
tests/components/switcher_kis/test_switch.py
Normal file
127
tests/components/switcher_kis/test_switch.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user