Updated changes for aioshelly 1.0.0 (#56083)

This commit is contained in:
Shay Levy 2021-09-11 00:48:55 +03:00 committed by GitHub
parent ac1251c52b
commit 8c3c2ad8e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 108 additions and 86 deletions

View file

@ -7,6 +7,7 @@ import logging
from typing import Any, Final, cast from typing import Any, Final, cast
import aioshelly import aioshelly
from aioshelly.block_device import BlockDevice
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
@ -89,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
temperature_unit = "C" if hass.config.units.is_metric else "F" temperature_unit = "C" if hass.config.units.is_metric else "F"
options = aioshelly.ConnectionOptions( options = aioshelly.common.ConnectionOptions(
entry.data[CONF_HOST], entry.data[CONF_HOST],
entry.data.get(CONF_USERNAME), entry.data.get(CONF_USERNAME),
entry.data.get(CONF_PASSWORD), entry.data.get(CONF_PASSWORD),
@ -98,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coap_context = await get_coap_context(hass) coap_context = await get_coap_context(hass)
device = await aioshelly.Device.create( device = await BlockDevice.create(
aiohttp_client.async_get_clientsession(hass), aiohttp_client.async_get_clientsession(hass),
coap_context, coap_context,
options, options,
@ -134,7 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("Setting up online device %s", entry.title) _LOGGER.debug("Setting up online device %s", entry.title)
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
await device.initialize(True) await device.initialize()
except (asyncio.TimeoutError, OSError) as err: except (asyncio.TimeoutError, OSError) as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
@ -146,7 +147,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"Setup for device %s will resume when device is online", entry.title "Setup for device %s will resume when device is online", entry.title
) )
device.subscribe_updates(_async_device_online) device.subscribe_updates(_async_device_online)
await device.coap_request("s")
else: else:
# Restore sensors for sleeping device # Restore sensors for sleeping device
_LOGGER.debug("Setting up offline device %s", entry.title) _LOGGER.debug("Setting up offline device %s", entry.title)
@ -156,7 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_device_setup( async def async_device_setup(
hass: HomeAssistant, entry: ConfigEntry, device: aioshelly.Device hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice
) -> None: ) -> None:
"""Set up a device that is online.""" """Set up a device that is online."""
device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
@ -179,7 +179,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
"""Wrapper for a Shelly device with Home Assistant specific functions.""" """Wrapper for a Shelly device with Home Assistant specific functions."""
def __init__( def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, device: aioshelly.Device self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice
) -> None: ) -> None:
"""Initialize the Shelly device wrapper.""" """Initialize the Shelly device wrapper."""
self.device_id: str | None = None self.device_id: str | None = None
@ -208,7 +208,9 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
) )
self._last_input_events_count: dict = {} self._last_input_events_count: dict = {}
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
)
@callback @callback
def _async_device_updates_handler(self) -> None: def _async_device_updates_handler(self) -> None:
@ -216,6 +218,8 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
if not self.device.initialized: if not self.device.initialized:
return return
assert self.device.blocks
# For buttons which are battery powered - set initial value for last_event_count # For buttons which are battery powered - set initial value for last_event_count
if self.model in SHBTN_MODELS and self._last_input_events_count.get(1) is None: if self.model in SHBTN_MODELS and self._last_input_events_count.get(1) is None:
for block in self.device.blocks: for block in self.device.blocks:
@ -298,7 +302,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
# This is duplicate but otherwise via_device can't work # This is duplicate but otherwise via_device can't work
identifiers={(DOMAIN, self.mac)}, identifiers={(DOMAIN, self.mac)},
manufacturer="Shelly", manufacturer="Shelly",
model=aioshelly.MODEL_NAMES.get(self.model, self.model), model=aioshelly.const.MODEL_NAMES.get(self.model, self.model),
sw_version=sw_version, sw_version=sw_version,
) )
self.device_id = entry.id self.device_id = entry.id
@ -306,10 +310,8 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
def shutdown(self) -> None: def shutdown(self) -> None:
"""Shutdown the wrapper.""" """Shutdown the wrapper."""
if self.device: self.device.shutdown()
self.device.shutdown() self._async_remove_device_updates_handler()
self._async_remove_device_updates_handler()
self.device = None
@callback @callback
def _handle_ha_stop(self, _event: Event) -> None: def _handle_ha_stop(self, _event: Event) -> None:
@ -321,7 +323,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
"""Rest Wrapper for a Shelly device with Home Assistant specific functions.""" """Rest Wrapper for a Shelly device with Home Assistant specific functions."""
def __init__(self, hass: HomeAssistant, device: aioshelly.Device) -> None: def __init__(self, hass: HomeAssistant, device: BlockDevice) -> None:
"""Initialize the Shelly device wrapper.""" """Initialize the Shelly device wrapper."""
if ( if (
device.settings["device"]["type"] device.settings["device"]["type"]

View file

@ -48,7 +48,7 @@ SENSORS: Final = {
("sensor", "dwIsOpened"): BlockAttributeDescription( ("sensor", "dwIsOpened"): BlockAttributeDescription(
name="Door", name="Door",
device_class=DEVICE_CLASS_OPENING, device_class=DEVICE_CLASS_OPENING,
available=lambda block: cast(bool, block.dwIsOpened != -1), available=lambda block: cast(int, block.dwIsOpened) != -1,
), ),
("sensor", "flood"): BlockAttributeDescription( ("sensor", "flood"): BlockAttributeDescription(
name="Flood", device_class=DEVICE_CLASS_MOISTURE name="Flood", device_class=DEVICE_CLASS_MOISTURE

View file

@ -7,6 +7,7 @@ from typing import Any, Dict, Final, cast
import aiohttp import aiohttp
import aioshelly import aioshelly
from aioshelly.block_device import BlockDevice
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
@ -39,13 +40,13 @@ async def validate_input(
Data has the keys from DATA_SCHEMA with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
""" """
options = aioshelly.ConnectionOptions( options = aioshelly.common.ConnectionOptions(
host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD) host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)
) )
coap_context = await get_coap_context(hass) coap_context = await get_coap_context(hass)
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
device = await aioshelly.Device.create( device = await BlockDevice.create(
aiohttp_client.async_get_clientsession(hass), aiohttp_client.async_get_clientsession(hass),
coap_context, coap_context,
options, options,
@ -82,7 +83,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
info = await self._async_get_info(host) info = await self._async_get_info(host)
except HTTP_CONNECT_ERRORS: except HTTP_CONNECT_ERRORS:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except aioshelly.FirmwareUnsupported: except aioshelly.exceptions.FirmwareUnsupported:
return self.async_abort(reason="unsupported_firmware") return self.async_abort(reason="unsupported_firmware")
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
@ -165,7 +166,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.info = info = await self._async_get_info(discovery_info["host"]) self.info = info = await self._async_get_info(discovery_info["host"])
except HTTP_CONNECT_ERRORS: except HTTP_CONNECT_ERRORS:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except aioshelly.FirmwareUnsupported: except aioshelly.exceptions.FirmwareUnsupported:
return self.async_abort(reason="unsupported_firmware") return self.async_abort(reason="unsupported_firmware")
await self.async_set_unique_id(info["mac"]) await self.async_set_unique_id(info["mac"])
@ -206,7 +207,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form( return self.async_show_form(
step_id="confirm_discovery", step_id="confirm_discovery",
description_placeholders={ description_placeholders={
"model": aioshelly.MODEL_NAMES.get( "model": aioshelly.const.MODEL_NAMES.get(
self.info["type"], self.info["type"] self.info["type"], self.info["type"]
), ),
"host": self.host, "host": self.host,
@ -219,7 +220,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return cast( return cast(
Dict[str, Any], Dict[str, Any],
await aioshelly.get_info( await aioshelly.common.get_info(
aiohttp_client.async_get_clientsession(self.hass), aiohttp_client.async_get_clientsession(self.hass),
host, host,
), ),

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any, cast from typing import Any, cast
from aioshelly import Block from aioshelly.block_device import Block
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
@ -57,7 +57,7 @@ class ShellyCover(ShellyBlockEntity, CoverEntity):
if self.control_result: if self.control_result:
return cast(bool, self.control_result["current_pos"] == 0) return cast(bool, self.control_result["current_pos"] == 0)
return cast(bool, self.block.rollerPos == 0) return cast(int, self.block.rollerPos) == 0
@property @property
def current_cover_position(self) -> int: def current_cover_position(self) -> int:
@ -73,7 +73,7 @@ class ShellyCover(ShellyBlockEntity, CoverEntity):
if self.control_result: if self.control_result:
return cast(bool, self.control_result["state"] == "close") return cast(bool, self.control_result["state"] == "close")
return cast(bool, self.block.roller == "close") return self.block.roller == "close"
@property @property
def is_opening(self) -> bool: def is_opening(self) -> bool:
@ -81,7 +81,7 @@ class ShellyCover(ShellyBlockEntity, CoverEntity):
if self.control_result: if self.control_result:
return cast(bool, self.control_result["state"] == "open") return cast(bool, self.control_result["state"] == "open")
return cast(bool, self.block.roller == "open") return self.block.roller == "open"
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:

View file

@ -60,6 +60,8 @@ async def async_validate_trigger_config(
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
assert wrapper.device.blocks
for block in wrapper.device.blocks: for block in wrapper.device.blocks:
input_triggers = get_input_triggers(wrapper.device, block) input_triggers = get_input_triggers(wrapper.device, block)
if trigger in input_triggers: if trigger in input_triggers:
@ -93,6 +95,8 @@ async def async_get_triggers(
) )
return triggers return triggers
assert wrapper.device.blocks
for block in wrapper.device.blocks: for block in wrapper.device.blocks:
input_triggers = get_input_triggers(wrapper.device, block) input_triggers = get_input_triggers(wrapper.device, block)

View file

@ -6,7 +6,7 @@ from dataclasses import dataclass
import logging import logging
from typing import Any, Callable, Final, cast from typing import Any, Callable, Final, cast
import aioshelly from aioshelly.block_device import Block
import async_timeout import async_timeout
from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.sensor import ATTR_STATE_CLASS
@ -62,6 +62,8 @@ async def async_setup_block_attribute_entities(
"""Set up entities for block attributes.""" """Set up entities for block attributes."""
blocks = [] blocks = []
assert wrapper.device.blocks
for block in wrapper.device.blocks: for block in wrapper.device.blocks:
for sensor_id in block.sensor_ids: for sensor_id in block.sensor_ids:
description = sensors.get((block.type, sensor_id)) description = sensors.get((block.type, sensor_id))
@ -175,10 +177,10 @@ class BlockAttributeDescription:
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
available: Callable[[aioshelly.Block], bool] | None = None available: Callable[[Block], bool] | None = None
# Callable (settings, block), return true if entity should be removed # Callable (settings, block), return true if entity should be removed
removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None removal_condition: Callable[[dict, Block], bool] | None = None
extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None extra_state_attributes: Callable[[Block], dict | None] | None = None
@dataclass @dataclass
@ -198,7 +200,7 @@ class RestAttributeDescription:
class ShellyBlockEntity(entity.Entity): class ShellyBlockEntity(entity.Entity):
"""Helper class to represent a block.""" """Helper class to represent a block."""
def __init__(self, wrapper: ShellyDeviceWrapper, block: aioshelly.Block) -> None: def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
"""Initialize Shelly entity.""" """Initialize Shelly entity."""
self.wrapper = wrapper self.wrapper = wrapper
self.block = block self.block = block
@ -267,7 +269,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
def __init__( def __init__(
self, self,
wrapper: ShellyDeviceWrapper, wrapper: ShellyDeviceWrapper,
block: aioshelly.Block, block: Block,
attribute: str, attribute: str,
description: BlockAttributeDescription, description: BlockAttributeDescription,
) -> None: ) -> None:
@ -418,7 +420,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
def __init__( def __init__(
self, self,
wrapper: ShellyDeviceWrapper, wrapper: ShellyDeviceWrapper,
block: aioshelly.Block, block: Block | None,
attribute: str, attribute: str,
description: BlockAttributeDescription, description: BlockAttributeDescription,
entry: entity_registry.RegistryEntry | None = None, entry: entity_registry.RegistryEntry | None = None,
@ -429,7 +431,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
self.last_state: StateType = None self.last_state: StateType = None
self.wrapper = wrapper self.wrapper = wrapper
self.attribute = attribute self.attribute = attribute
self.block = block self.block: Block | None = block # type: ignore[assignment]
self.description = description self.description = description
self._unit = self.description.unit self._unit = self.description.unit
@ -468,6 +470,8 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
_, entity_block, entity_sensor = self.unique_id.split("-") _, entity_block, entity_sensor = self.unique_id.split("-")
assert self.wrapper.device.blocks
for block in self.wrapper.device.blocks: for block in self.wrapper.device.blocks:
if block.description != entity_block: if block.description != entity_block:
continue continue

View file

@ -5,7 +5,7 @@ import asyncio
import logging import logging
from typing import Any, Final, cast from typing import Any, Final, cast
from aioshelly import Block from aioshelly.block_device import Block
import async_timeout import async_timeout
from homeassistant.components.light import ( from homeassistant.components.light import (
@ -117,7 +117,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
self._supported_features |= SUPPORT_EFFECT self._supported_features |= SUPPORT_EFFECT
if wrapper.model in MODELS_SUPPORTING_LIGHT_TRANSITION: if wrapper.model in MODELS_SUPPORTING_LIGHT_TRANSITION:
match = FIRMWARE_PATTERN.search(wrapper.device.settings.get("fw")) match = FIRMWARE_PATTERN.search(wrapper.device.settings.get("fw", ""))
if ( if (
match is not None match is not None
and int(match[0]) >= LIGHT_TRANSITION_MIN_FIRMWARE_DATE and int(match[0]) >= LIGHT_TRANSITION_MIN_FIRMWARE_DATE

View file

@ -3,7 +3,7 @@
"name": "Shelly", "name": "Shelly",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly", "documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==0.6.4"], "requirements": ["aioshelly==1.0.0"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View file

@ -40,7 +40,7 @@ SENSORS: Final = {
device_class=sensor.DEVICE_CLASS_BATTERY, device_class=sensor.DEVICE_CLASS_BATTERY,
state_class=sensor.STATE_CLASS_MEASUREMENT, state_class=sensor.STATE_CLASS_MEASUREMENT,
removal_condition=lambda settings, _: settings.get("external_power") == 1, removal_condition=lambda settings, _: settings.get("external_power") == 1,
available=lambda block: cast(bool, block.battery != -1), available=lambda block: cast(int, block.battery) != -1,
), ),
("device", "deviceTemp"): BlockAttributeDescription( ("device", "deviceTemp"): BlockAttributeDescription(
name="Device Temperature", name="Device Temperature",
@ -162,7 +162,7 @@ SENSORS: Final = {
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_TEMPERATURE, device_class=sensor.DEVICE_CLASS_TEMPERATURE,
state_class=sensor.STATE_CLASS_MEASUREMENT, state_class=sensor.STATE_CLASS_MEASUREMENT,
available=lambda block: cast(bool, block.extTemp != 999), available=lambda block: cast(int, block.extTemp) != 999,
), ),
("sensor", "humidity"): BlockAttributeDescription( ("sensor", "humidity"): BlockAttributeDescription(
name="Humidity", name="Humidity",
@ -170,14 +170,14 @@ SENSORS: Final = {
value=lambda value: round(value, 1), value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_HUMIDITY, device_class=sensor.DEVICE_CLASS_HUMIDITY,
state_class=sensor.STATE_CLASS_MEASUREMENT, state_class=sensor.STATE_CLASS_MEASUREMENT,
available=lambda block: cast(bool, block.extTemp != 999), available=lambda block: cast(int, block.extTemp) != 999,
), ),
("sensor", "luminosity"): BlockAttributeDescription( ("sensor", "luminosity"): BlockAttributeDescription(
name="Luminosity", name="Luminosity",
unit=LIGHT_LUX, unit=LIGHT_LUX,
device_class=sensor.DEVICE_CLASS_ILLUMINANCE, device_class=sensor.DEVICE_CLASS_ILLUMINANCE,
state_class=sensor.STATE_CLASS_MEASUREMENT, state_class=sensor.STATE_CLASS_MEASUREMENT,
available=lambda block: cast(bool, block.luminosity != -1), available=lambda block: cast(int, block.luminosity) != -1,
), ),
("sensor", "tilt"): BlockAttributeDescription( ("sensor", "tilt"): BlockAttributeDescription(
name="Tilt", name="Tilt",
@ -191,7 +191,7 @@ SENSORS: Final = {
icon="mdi:progress-wrench", icon="mdi:progress-wrench",
value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1), value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1),
extra_state_attributes=lambda block: { extra_state_attributes=lambda block: {
"Operational hours": round(block.totalWorkTime / 3600, 1) "Operational hours": round(cast(int, block.totalWorkTime) / 3600, 1)
}, },
), ),
("adc", "adc"): BlockAttributeDescription( ("adc", "adc"): BlockAttributeDescription(

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any, cast from typing import Any, cast
from aioshelly import Block from aioshelly.block_device import Block
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry

View file

@ -5,7 +5,7 @@ from datetime import datetime, timedelta
import logging import logging
from typing import Any, Final, cast from typing import Any, Final, cast
import aioshelly from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -40,18 +40,20 @@ async def async_remove_shelly_entity(
def temperature_unit(block_info: dict[str, Any]) -> str: def temperature_unit(block_info: dict[str, Any]) -> str:
"""Detect temperature unit.""" """Detect temperature unit."""
if block_info[aioshelly.BLOCK_VALUE_UNIT] == "F": if block_info[BLOCK_VALUE_UNIT] == "F":
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
return TEMP_CELSIUS return TEMP_CELSIUS
def get_device_name(device: aioshelly.Device) -> str: def get_device_name(device: BlockDevice) -> str:
"""Naming for device.""" """Naming for device."""
return cast(str, device.settings["name"] or device.settings["device"]["hostname"]) return cast(str, device.settings["name"] or device.settings["device"]["hostname"])
def get_number_of_channels(device: aioshelly.Device, block: aioshelly.Block) -> int: def get_number_of_channels(device: BlockDevice, block: Block) -> int:
"""Get number of channels for block type.""" """Get number of channels for block type."""
assert isinstance(device.shelly, dict)
channels = None channels = None
if block.type == "input": if block.type == "input":
@ -71,8 +73,8 @@ def get_number_of_channels(device: aioshelly.Device, block: aioshelly.Block) ->
def get_entity_name( def get_entity_name(
device: aioshelly.Device, device: BlockDevice,
block: aioshelly.Block, block: Block | None,
description: str | None = None, description: str | None = None,
) -> str: ) -> str:
"""Naming for switch and sensors.""" """Naming for switch and sensors."""
@ -84,10 +86,7 @@ def get_entity_name(
return channel_name return channel_name
def get_device_channel_name( def get_device_channel_name(device: BlockDevice, block: Block | None) -> str:
device: aioshelly.Device,
block: aioshelly.Block,
) -> str:
"""Get name based on device and channel name.""" """Get name based on device and channel name."""
entity_name = get_device_name(device) entity_name = get_device_name(device)
@ -98,8 +97,10 @@ def get_device_channel_name(
): ):
return entity_name return entity_name
assert block.channel
channel_name: str | None = None channel_name: str | None = None
mode = block.type + "s" mode = cast(str, block.type) + "s"
if mode in device.settings: if mode in device.settings:
channel_name = device.settings[mode][int(block.channel)].get("name") channel_name = device.settings[mode][int(block.channel)].get("name")
@ -114,7 +115,7 @@ def get_device_channel_name(
return f"{entity_name} channel {chr(int(block.channel)+base)}" return f"{entity_name} channel {chr(int(block.channel)+base)}"
def is_momentary_input(settings: dict[str, Any], block: aioshelly.Block) -> bool: def is_momentary_input(settings: dict[str, Any], block: Block) -> bool:
"""Return true if input button settings is set to a momentary type.""" """Return true if input button settings is set to a momentary type."""
# Shelly Button type is fixed to momentary and no btn_type # Shelly Button type is fixed to momentary and no btn_type
if settings["device"]["type"] in SHBTN_MODELS: if settings["device"]["type"] in SHBTN_MODELS:
@ -150,9 +151,7 @@ def get_device_uptime(status: dict[str, Any], last_uptime: str | None) -> str:
return last_uptime return last_uptime
def get_input_triggers( def get_input_triggers(device: BlockDevice, block: Block) -> list[tuple[str, str]]:
device: aioshelly.Device, block: aioshelly.Block
) -> list[tuple[str, str]]:
"""Return list of input triggers for block.""" """Return list of input triggers for block."""
if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids: if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids:
return [] return []
@ -165,6 +164,7 @@ def get_input_triggers(
if block.type == "device" or get_number_of_channels(device, block) == 1: if block.type == "device" or get_number_of_channels(device, block) == 1:
subtype = "button" subtype = "button"
else: else:
assert block.channel
subtype = f"button{int(block.channel)+1}" subtype = f"button{int(block.channel)+1}"
if device.settings["device"]["type"] in SHBTN_MODELS: if device.settings["device"]["type"] in SHBTN_MODELS:
@ -181,9 +181,9 @@ def get_input_triggers(
@singleton.singleton("shelly_coap") @singleton.singleton("shelly_coap")
async def get_coap_context(hass: HomeAssistant) -> aioshelly.COAP: async def get_coap_context(hass: HomeAssistant) -> COAP:
"""Get CoAP context to be used in all Shelly devices.""" """Get CoAP context to be used in all Shelly devices."""
context = aioshelly.COAP() context = COAP()
if DOMAIN in hass.data: if DOMAIN in hass.data:
port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT) port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT)
else: else:

View file

@ -240,7 +240,7 @@ aiopylgtv==0.4.0
aiorecollect==1.0.8 aiorecollect==1.0.8
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==0.6.4 aioshelly==1.0.0
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==2.0.5 aioswitcher==2.0.5

View file

@ -161,7 +161,7 @@ aiopylgtv==0.4.0
aiorecollect==1.0.8 aiorecollect==1.0.8
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==0.6.4 aioshelly==1.0.0
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==2.0.5 aioswitcher==2.0.5

View file

@ -32,10 +32,10 @@ async def test_form(hass):
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
), patch( ), patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,
@ -78,10 +78,10 @@ async def test_title_without_name(hass):
settings["device"] = settings["device"].copy() settings["device"] = settings["device"].copy()
settings["device"]["hostname"] = "shelly1pm-12345" settings["device"]["hostname"] = "shelly1pm-12345"
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
), patch( ), patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
settings=settings, settings=settings,
@ -119,7 +119,7 @@ async def test_form_auth(hass):
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True},
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -131,7 +131,7 @@ async def test_form_auth(hass):
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,
@ -172,7 +172,7 @@ async def test_form_errors_get_info(hass, error):
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with patch("aioshelly.get_info", side_effect=exc): with patch("aioshelly.common.get_info", side_effect=exc):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"host": "1.1.1.1"}, {"host": "1.1.1.1"},
@ -193,8 +193,10 @@ async def test_form_errors_test_connection(hass, error):
) )
with patch( with patch(
"aioshelly.get_info", return_value={"mac": "test-mac", "auth": False} "aioshelly.common.get_info", return_value={"mac": "test-mac", "auth": False}
), patch("aioshelly.Device.create", new=AsyncMock(side_effect=exc)): ), patch(
"aioshelly.block_device.BlockDevice.create", new=AsyncMock(side_effect=exc)
):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"host": "1.1.1.1"}, {"host": "1.1.1.1"},
@ -217,7 +219,7 @@ async def test_form_already_configured(hass):
) )
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -252,10 +254,10 @@ async def test_user_setup_ignored_device(hass):
settings["fw"] = "20201124-092534/v1.9.0@57ac4ad8" settings["fw"] = "20201124-092534/v1.9.0@57ac4ad8"
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
), patch( ), patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
settings=settings, settings=settings,
@ -287,7 +289,10 @@ async def test_form_firmware_unsupported(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with patch("aioshelly.get_info", side_effect=aioshelly.FirmwareUnsupported): with patch(
"aioshelly.common.get_info",
side_effect=aioshelly.exceptions.FirmwareUnsupported,
):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"host": "1.1.1.1"}, {"host": "1.1.1.1"},
@ -313,14 +318,17 @@ async def test_form_auth_errors_test_connection(hass, error):
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with patch("aioshelly.get_info", return_value={"mac": "test-mac", "auth": True}): with patch(
"aioshelly.common.get_info",
return_value={"mac": "test-mac", "auth": True},
):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"host": "1.1.1.1"}, {"host": "1.1.1.1"},
) )
with patch( with patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock(side_effect=exc), new=AsyncMock(side_effect=exc),
): ):
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
@ -336,10 +344,10 @@ async def test_zeroconf(hass):
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
), patch( ), patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,
@ -388,7 +396,7 @@ async def test_zeroconf_sleeping_device(hass):
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={ return_value={
"mac": "test-mac", "mac": "test-mac",
"type": "SHSW-1", "type": "SHSW-1",
@ -396,7 +404,7 @@ async def test_zeroconf_sleeping_device(hass):
"sleep_mode": True, "sleep_mode": True,
}, },
), patch( ), patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
settings={ settings={
@ -460,7 +468,7 @@ async def test_zeroconf_sleeping_device_error(hass, error):
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={ return_value={
"mac": "test-mac", "mac": "test-mac",
"type": "SHSW-1", "type": "SHSW-1",
@ -468,7 +476,7 @@ async def test_zeroconf_sleeping_device_error(hass, error):
"sleep_mode": True, "sleep_mode": True,
}, },
), patch( ), patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock(side_effect=exc), new=AsyncMock(side_effect=exc),
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -489,7 +497,7 @@ async def test_zeroconf_already_configured(hass):
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -506,7 +514,10 @@ async def test_zeroconf_already_configured(hass):
async def test_zeroconf_firmware_unsupported(hass): async def test_zeroconf_firmware_unsupported(hass):
"""Test we abort if device firmware is unsupported.""" """Test we abort if device firmware is unsupported."""
with patch("aioshelly.get_info", side_effect=aioshelly.FirmwareUnsupported): with patch(
"aioshelly.common.get_info",
side_effect=aioshelly.exceptions.FirmwareUnsupported,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
data=DISCOVERY_INFO, data=DISCOVERY_INFO,
@ -519,7 +530,7 @@ async def test_zeroconf_firmware_unsupported(hass):
async def test_zeroconf_cannot_connect(hass): async def test_zeroconf_cannot_connect(hass):
"""Test we get the form.""" """Test we get the form."""
with patch("aioshelly.get_info", side_effect=asyncio.TimeoutError): with patch("aioshelly.common.get_info", side_effect=asyncio.TimeoutError):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
data=DISCOVERY_INFO, data=DISCOVERY_INFO,
@ -534,7 +545,7 @@ async def test_zeroconf_require_auth(hass):
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"aioshelly.get_info", "aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True}, return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True},
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -546,7 +557,7 @@ async def test_zeroconf_require_auth(hass):
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch(
"aioshelly.Device.create", "aioshelly.block_device.BlockDevice.create",
new=AsyncMock( new=AsyncMock(
return_value=Mock( return_value=Mock(
settings=MOCK_SETTINGS, settings=MOCK_SETTINGS,