Improve typing in Shelly integration (#52544)

This commit is contained in:
Maciej Bieniek 2021-07-21 19:11:44 +02:00 committed by GitHub
parent f128bc9ef8
commit 772cbd59d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 365 additions and 245 deletions

View file

@ -74,6 +74,7 @@ homeassistant.components.remote.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.select.* homeassistant.components.select.*
homeassistant.components.sensor.* homeassistant.components.sensor.*
homeassistant.components.shelly.*
homeassistant.components.slack.* homeassistant.components.slack.*
homeassistant.components.sonos.media_player homeassistant.components.sonos.media_player
homeassistant.components.ssdp.* homeassistant.components.ssdp.*

View file

@ -1,7 +1,10 @@
"""The Shelly integration.""" """The Shelly integration."""
from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any, Final, cast
import aioshelly import aioshelly
import async_timeout import async_timeout
@ -15,10 +18,11 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
AIOSHELLY_DEVICE_TIMEOUT_SEC, AIOSHELLY_DEVICE_TIMEOUT_SEC,
@ -43,19 +47,19 @@ from .const import (
) )
from .utils import get_coap_context, get_device_name, get_device_sleep_period from .utils import get_coap_context, get_device_name, get_device_sleep_period
PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] PLATFORMS: Final = ["binary_sensor", "cover", "light", "sensor", "switch"]
SLEEPING_PLATFORMS = ["binary_sensor", "sensor"] SLEEPING_PLATFORMS: Final = ["binary_sensor", "sensor"]
_LOGGER = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
COAP_SCHEMA = vol.Schema( COAP_SCHEMA: Final = vol.Schema(
{ {
vol.Optional(CONF_COAP_PORT, default=DEFAULT_COAP_PORT): cv.port, vol.Optional(CONF_COAP_PORT, default=DEFAULT_COAP_PORT): cv.port,
} }
) )
CONFIG_SCHEMA = vol.Schema({DOMAIN: COAP_SCHEMA}, extra=vol.ALLOW_EXTRA) CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: COAP_SCHEMA}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Shelly component.""" """Set up the Shelly component."""
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
@ -113,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
sleep_period = entry.data.get("sleep_period") sleep_period = entry.data.get("sleep_period")
@callback @callback
def _async_device_online(_): def _async_device_online(_: Any) -> None:
_LOGGER.debug("Device %s is online, resuming setup", entry.title) _LOGGER.debug("Device %s is online, resuming setup", entry.title)
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None
@ -153,7 +157,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: aioshelly.Device
): ) -> 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][
COAP COAP
@ -174,9 +178,11 @@ async def async_device_setup(
class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): 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__(self, hass, entry, device: aioshelly.Device): def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, device: aioshelly.Device
) -> None:
"""Initialize the Shelly device wrapper.""" """Initialize the Shelly device wrapper."""
self.device_id = None self.device_id: str | None = None
sleep_period = entry.data["sleep_period"] sleep_period = entry.data["sleep_period"]
if sleep_period: if sleep_period:
@ -205,7 +211,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
@callback @callback
def _async_device_updates_handler(self): def _async_device_updates_handler(self) -> None:
"""Handle device updates.""" """Handle device updates."""
if not self.device.initialized: if not self.device.initialized:
return return
@ -258,7 +264,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
self.name, self.name,
) )
async def _async_update_data(self): async def _async_update_data(self) -> None:
"""Fetch data.""" """Fetch data."""
if self.entry.data.get("sleep_period"): if self.entry.data.get("sleep_period"):
# Sleeping device, no point polling it, just mark it unavailable # Sleeping device, no point polling it, just mark it unavailable
@ -267,21 +273,21 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
_LOGGER.debug("Polling Shelly Device - %s", self.name) _LOGGER.debug("Polling Shelly Device - %s", self.name)
try: try:
async with async_timeout.timeout(POLLING_TIMEOUT_SEC): async with async_timeout.timeout(POLLING_TIMEOUT_SEC):
return await self.device.update() await self.device.update()
except OSError as err: except OSError as err:
raise update_coordinator.UpdateFailed("Error fetching data") from err raise update_coordinator.UpdateFailed("Error fetching data") from err
@property @property
def model(self): def model(self) -> str:
"""Model of the device.""" """Model of the device."""
return self.entry.data["model"] return cast(str, self.entry.data["model"])
@property @property
def mac(self): def mac(self) -> str:
"""Mac address of the device.""" """Mac address of the device."""
return self.entry.unique_id return cast(str, self.entry.unique_id)
async def async_setup(self): async def async_setup(self) -> None:
"""Set up the wrapper.""" """Set up the wrapper."""
dev_reg = await device_registry.async_get_registry(self.hass) dev_reg = await device_registry.async_get_registry(self.hass)
sw_version = self.device.settings["fw"] if self.device.initialized else "" sw_version = self.device.settings["fw"] if self.device.initialized else ""
@ -298,7 +304,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
self.device_id = entry.id self.device_id = entry.id
self.device.subscribe_updates(self.async_set_updated_data) self.device.subscribe_updates(self.async_set_updated_data)
def shutdown(self): def shutdown(self) -> None:
"""Shutdown the wrapper.""" """Shutdown the wrapper."""
if self.device: if self.device:
self.device.shutdown() self.device.shutdown()
@ -306,7 +312,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
self.device = None self.device = None
@callback @callback
def _handle_ha_stop(self, _): def _handle_ha_stop(self, _event: Event) -> None:
"""Handle Home Assistant stopping.""" """Handle Home Assistant stopping."""
_LOGGER.debug("Stopping ShellyDeviceWrapper for %s", self.name) _LOGGER.debug("Stopping ShellyDeviceWrapper for %s", self.name)
self.shutdown() self.shutdown()
@ -315,7 +321,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, device: aioshelly.Device): def __init__(self, hass: HomeAssistant, device: aioshelly.Device) -> None:
"""Initialize the Shelly device wrapper.""" """Initialize the Shelly device wrapper."""
if ( if (
device.settings["device"]["type"] device.settings["device"]["type"]
@ -335,22 +341,22 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
) )
self.device = device self.device = device
async def _async_update_data(self): async def _async_update_data(self) -> None:
"""Fetch data.""" """Fetch data."""
try: try:
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
_LOGGER.debug("REST update for %s", self.name) _LOGGER.debug("REST update for %s", self.name)
return await self.device.update_status() await self.device.update_status()
except OSError as err: except OSError as err:
raise update_coordinator.UpdateFailed("Error fetching data") from err raise update_coordinator.UpdateFailed("Error fetching data") from err
@property @property
def mac(self): def mac(self) -> str:
"""Mac address of the device.""" """Mac address of the device."""
return self.device.settings["device"]["mac"] return cast(str, self.device.settings["device"]["mac"])
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE) device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE)
if device is not None: if device is not None:
@ -370,3 +376,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id)
return unload_ok return unload_ok
def get_device_wrapper(
hass: HomeAssistant, device_id: str
) -> ShellyDeviceWrapper | None:
"""Get a Shelly device wrapper for the given device id."""
if not hass.data.get(DOMAIN):
return None
for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]:
wrapper: ShellyDeviceWrapper | None = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry
].get(COAP)
if wrapper and wrapper.device_id == device_id:
return wrapper
return None

View file

@ -1,4 +1,8 @@
"""Binary sensor for Shelly.""" """Binary sensor for Shelly."""
from __future__ import annotations
from typing import Final
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_GAS, DEVICE_CLASS_GAS,
@ -12,6 +16,9 @@ from homeassistant.components.binary_sensor import (
STATE_ON, STATE_ON,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import ( from .entity import (
BlockAttributeDescription, BlockAttributeDescription,
@ -24,7 +31,7 @@ from .entity import (
) )
from .utils import is_momentary_input from .utils import is_momentary_input
SENSORS = { SENSORS: Final = {
("device", "overtemp"): BlockAttributeDescription( ("device", "overtemp"): BlockAttributeDescription(
name="Overheating", device_class=DEVICE_CLASS_PROBLEM name="Overheating", device_class=DEVICE_CLASS_PROBLEM
), ),
@ -83,7 +90,7 @@ SENSORS = {
), ),
} }
REST_SENSORS = { REST_SENSORS: Final = {
"cloud": RestAttributeDescription( "cloud": RestAttributeDescription(
name="Cloud", name="Cloud",
value=lambda status, _: status["cloud"]["connected"], value=lambda status, _: status["cloud"]["connected"],
@ -103,7 +110,11 @@ REST_SENSORS = {
} }
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensors for device.""" """Set up sensors for device."""
if config_entry.data["sleep_period"]: if config_entry.data["sleep_period"]:
await async_setup_entry_attribute_entities( await async_setup_entry_attribute_entities(
@ -130,7 +141,7 @@ class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
"""Shelly binary sensor entity.""" """Shelly binary sensor entity."""
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if sensor state is on.""" """Return true if sensor state is on."""
return bool(self.attribute_value) return bool(self.attribute_value)
@ -139,7 +150,7 @@ class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity):
"""Shelly REST binary sensor entity.""" """Shelly REST binary sensor entity."""
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if REST sensor state is on.""" """Return true if REST sensor state is on."""
return bool(self.attribute_value) return bool(self.attribute_value)
@ -150,7 +161,7 @@ class ShellySleepingBinarySensor(
"""Represent a shelly sleeping binary sensor.""" """Represent a shelly sleeping binary sensor."""
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if sensor state is on.""" """Return true if sensor state is on."""
if self.block is not None: if self.block is not None:
return bool(self.attribute_value) return bool(self.attribute_value)

View file

@ -1,6 +1,9 @@
"""Config flow for Shelly integration.""" """Config flow for Shelly integration."""
from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import Any, Dict, Final, cast
import aiohttp import aiohttp
import aioshelly import aioshelly
@ -14,19 +17,23 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
HTTP_UNAUTHORIZED, HTTP_UNAUTHORIZED,
) )
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, DOMAIN from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, DOMAIN
from .utils import get_coap_context, get_device_sleep_period from .utils import get_coap_context, get_device_sleep_period
_LOGGER = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str})
HTTP_CONNECT_ERRORS = (asyncio.TimeoutError, aiohttp.ClientError) HTTP_CONNECT_ERRORS: Final = (asyncio.TimeoutError, aiohttp.ClientError)
async def validate_input(hass: core.HomeAssistant, host, data): async def validate_input(
hass: core.HomeAssistant, host: str, data: dict[str, Any]
) -> dict[str, Any]:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
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.
@ -60,15 +67,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
host = None host: str = ""
info = None info: dict[str, Any] = {}
device_info = None device_info: dict[str, Any] = {}
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
host = user_input[CONF_HOST] host: str = user_input[CONF_HOST]
try: try:
info = await self._async_get_info(host) info = await self._async_get_info(host)
except HTTP_CONNECT_ERRORS: except HTTP_CONNECT_ERRORS:
@ -106,9 +115,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=HOST_SCHEMA, errors=errors step_id="user", data_schema=HOST_SCHEMA, errors=errors
) )
async def async_step_credentials(self, user_input=None): async def async_step_credentials(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the credentials step.""" """Handle the credentials step."""
errors = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
try: try:
device_info = await validate_input(self.hass, self.host, user_input) device_info = await validate_input(self.hass, self.host, user_input)
@ -146,7 +157,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="credentials", data_schema=schema, errors=errors step_id="credentials", data_schema=schema, errors=errors
) )
async def async_step_zeroconf(self, discovery_info): async def async_step_zeroconf(
self, discovery_info: DiscoveryInfoType
) -> FlowResult:
"""Handle zeroconf discovery.""" """Handle zeroconf discovery."""
try: try:
self.info = info = await self._async_get_info(discovery_info["host"]) self.info = info = await self._async_get_info(discovery_info["host"])
@ -173,9 +186,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_confirm_discovery() return await self.async_step_confirm_discovery()
async def async_step_confirm_discovery(self, user_input=None): async def async_step_confirm_discovery(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle discovery confirm.""" """Handle discovery confirm."""
errors = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
return self.async_create_entry( return self.async_create_entry(
title=self.device_info["title"] or self.device_info["hostname"], title=self.device_info["title"] or self.device_info["hostname"],
@ -199,10 +214,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def _async_get_info(self, host): async def _async_get_info(self, host: str) -> dict[str, Any]:
"""Get info from shelly device.""" """Get info from shelly device."""
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
return await aioshelly.get_info( return cast(
aiohttp_client.async_get_clientsession(self.hass), Dict[str, Any],
host, await aioshelly.get_info(
aiohttp_client.async_get_clientsession(self.hass),
host,
),
) )

View file

@ -1,34 +1,37 @@
"""Constants for the Shelly integration.""" """Constants for the Shelly integration."""
from __future__ import annotations
COAP = "coap" from typing import Final
DATA_CONFIG_ENTRY = "config_entry"
DEVICE = "device"
DOMAIN = "shelly"
REST = "rest"
CONF_COAP_PORT = "coap_port" COAP: Final = "coap"
DEFAULT_COAP_PORT = 5683 DATA_CONFIG_ENTRY: Final = "config_entry"
DEVICE: Final = "device"
DOMAIN: Final = "shelly"
REST: Final = "rest"
CONF_COAP_PORT: Final = "coap_port"
DEFAULT_COAP_PORT: Final = 5683
# Used in "_async_update_data" as timeout for polling data from devices. # Used in "_async_update_data" as timeout for polling data from devices.
POLLING_TIMEOUT_SEC = 18 POLLING_TIMEOUT_SEC: Final = 18
# Refresh interval for REST sensors # Refresh interval for REST sensors
REST_SENSORS_UPDATE_INTERVAL = 60 REST_SENSORS_UPDATE_INTERVAL: Final = 60
# Timeout used for aioshelly calls # Timeout used for aioshelly calls
AIOSHELLY_DEVICE_TIMEOUT_SEC = 10 AIOSHELLY_DEVICE_TIMEOUT_SEC: Final = 10
# Multiplier used to calculate the "update_interval" for sleeping devices. # Multiplier used to calculate the "update_interval" for sleeping devices.
SLEEP_PERIOD_MULTIPLIER = 1.2 SLEEP_PERIOD_MULTIPLIER: Final = 1.2
# Multiplier used to calculate the "update_interval" for non-sleeping devices. # Multiplier used to calculate the "update_interval" for non-sleeping devices.
UPDATE_PERIOD_MULTIPLIER = 2.2 UPDATE_PERIOD_MULTIPLIER: Final = 2.2
# Shelly Air - Maximum work hours before lamp replacement # Shelly Air - Maximum work hours before lamp replacement
SHAIR_MAX_WORK_HOURS = 9000 SHAIR_MAX_WORK_HOURS: Final = 9000
# Map Shelly input events # Map Shelly input events
INPUTS_EVENTS_DICT = { INPUTS_EVENTS_DICT: Final = {
"S": "single", "S": "single",
"SS": "double", "SS": "double",
"SSS": "triple", "SSS": "triple",
@ -38,28 +41,20 @@ INPUTS_EVENTS_DICT = {
} }
# List of battery devices that maintain a permanent WiFi connection # List of battery devices that maintain a permanent WiFi connection
BATTERY_DEVICES_WITH_PERMANENT_CONNECTION = ["SHMOS-01"] BATTERY_DEVICES_WITH_PERMANENT_CONNECTION: Final = ["SHMOS-01"]
EVENT_SHELLY_CLICK = "shelly.click" EVENT_SHELLY_CLICK: Final = "shelly.click"
ATTR_CLICK_TYPE = "click_type" ATTR_CLICK_TYPE: Final = "click_type"
ATTR_CHANNEL = "channel" ATTR_CHANNEL: Final = "channel"
ATTR_DEVICE = "device" ATTR_DEVICE: Final = "device"
CONF_SUBTYPE = "subtype" CONF_SUBTYPE: Final = "subtype"
BASIC_INPUTS_EVENTS_TYPES = { BASIC_INPUTS_EVENTS_TYPES: Final = {"single", "long"}
"single",
"long",
}
SHBTN_INPUTS_EVENTS_TYPES = { SHBTN_INPUTS_EVENTS_TYPES: Final = {"single", "double", "triple", "long"}
"single",
"double",
"triple",
"long",
}
SUPPORTED_INPUTS_EVENTS_TYPES = SHIX3_1_INPUTS_EVENTS_TYPES = { SUPPORTED_INPUTS_EVENTS_TYPES: Final = {
"single", "single",
"double", "double",
"triple", "triple",
@ -68,23 +63,20 @@ SUPPORTED_INPUTS_EVENTS_TYPES = SHIX3_1_INPUTS_EVENTS_TYPES = {
"long_single", "long_single",
} }
INPUTS_EVENTS_SUBTYPES = { SHIX3_1_INPUTS_EVENTS_TYPES = SUPPORTED_INPUTS_EVENTS_TYPES
"button": 1,
"button1": 1,
"button2": 2,
"button3": 3,
}
SHBTN_MODELS = ["SHBTN-1", "SHBTN-2"] INPUTS_EVENTS_SUBTYPES: Final = {"button": 1, "button1": 1, "button2": 2, "button3": 3}
STANDARD_RGB_EFFECTS = { SHBTN_MODELS: Final = ["SHBTN-1", "SHBTN-2"]
STANDARD_RGB_EFFECTS: Final = {
0: "Off", 0: "Off",
1: "Meteor Shower", 1: "Meteor Shower",
2: "Gradual Change", 2: "Gradual Change",
3: "Flash", 3: "Flash",
} }
SHBLB_1_RGB_EFFECTS = { SHBLB_1_RGB_EFFECTS: Final = {
0: "Off", 0: "Off",
1: "Meteor Shower", 1: "Meteor Shower",
2: "Gradual Change", 2: "Gradual Change",
@ -95,8 +87,8 @@ SHBLB_1_RGB_EFFECTS = {
} }
# Kelvin value for colorTemp # Kelvin value for colorTemp
KELVIN_MAX_VALUE = 6500 KELVIN_MAX_VALUE: Final = 6500
KELVIN_MIN_VALUE_WHITE = 2700 KELVIN_MIN_VALUE_WHITE: Final = 2700
KELVIN_MIN_VALUE_COLOR = 3000 KELVIN_MIN_VALUE_COLOR: Final = 3000
UPTIME_DEVIATION = 5 UPTIME_DEVIATION: Final = 5

View file

@ -1,4 +1,8 @@
"""Cover for Shelly.""" """Cover for Shelly."""
from __future__ import annotations
from typing import Any, cast
from aioshelly import Block from aioshelly import Block
from homeassistant.components.cover import ( from homeassistant.components.cover import (
@ -10,14 +14,20 @@ from homeassistant.components.cover import (
SUPPORT_STOP, SUPPORT_STOP,
CoverEntity, CoverEntity,
) )
from homeassistant.core import callback from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ShellyDeviceWrapper from . import ShellyDeviceWrapper
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN
from .entity import ShellyBlockEntity from .entity import ShellyBlockEntity
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up cover for device.""" """Set up cover for device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
blocks = [block for block in wrapper.device.blocks if block.type == "roller"] blocks = [block for block in wrapper.device.blocks if block.type == "roller"]
@ -36,72 +46,72 @@ class ShellyCover(ShellyBlockEntity, CoverEntity):
def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
"""Initialize light.""" """Initialize light."""
super().__init__(wrapper, block) super().__init__(wrapper, block)
self.control_result = None self.control_result: dict[str, Any] | None = None
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP self._supported_features: int = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
if self.wrapper.device.settings["rollers"][0]["positioning"]: if self.wrapper.device.settings["rollers"][0]["positioning"]:
self._supported_features |= SUPPORT_SET_POSITION self._supported_features |= SUPPORT_SET_POSITION
@property @property
def is_closed(self): def is_closed(self) -> bool:
"""If cover is closed.""" """If cover is closed."""
if self.control_result: if self.control_result:
return self.control_result["current_pos"] == 0 return cast(bool, self.control_result["current_pos"] == 0)
return self.block.rollerPos == 0 return cast(bool, self.block.rollerPos == 0)
@property @property
def current_cover_position(self): def current_cover_position(self) -> int:
"""Position of the cover.""" """Position of the cover."""
if self.control_result: if self.control_result:
return self.control_result["current_pos"] return cast(int, self.control_result["current_pos"])
return self.block.rollerPos return cast(int, self.block.rollerPos)
@property @property
def is_closing(self): def is_closing(self) -> bool:
"""Return if the cover is closing.""" """Return if the cover is closing."""
if self.control_result: if self.control_result:
return self.control_result["state"] == "close" return cast(bool, self.control_result["state"] == "close")
return self.block.roller == "close" return cast(bool, self.block.roller == "close")
@property @property
def is_opening(self): def is_opening(self) -> bool:
"""Return if the cover is opening.""" """Return if the cover is opening."""
if self.control_result: if self.control_result:
return self.control_result["state"] == "open" return cast(bool, self.control_result["state"] == "open")
return self.block.roller == "open" return cast(bool, self.block.roller == "open")
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
return self._supported_features return self._supported_features
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover.""" """Close cover."""
self.control_result = await self.set_state(go="close") self.control_result = await self.set_state(go="close")
self.async_write_ha_state() self.async_write_ha_state()
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs: Any) -> None:
"""Open cover.""" """Open cover."""
self.control_result = await self.set_state(go="open") self.control_result = await self.set_state(go="open")
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
self.control_result = await self.set_state( self.control_result = await self.set_state(
go="to_pos", roller_pos=kwargs[ATTR_POSITION] go="to_pos", roller_pos=kwargs[ATTR_POSITION]
) )
self.async_write_ha_state() self.async_write_ha_state()
async def async_stop_cover(self, **_kwargs): async def async_stop_cover(self, **_kwargs: Any) -> None:
"""Stop the cover.""" """Stop the cover."""
self.control_result = await self.set_state(go="stop") self.control_result = await self.set_state(go="stop")
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _update_callback(self): def _update_callback(self) -> None:
"""When device updates, clear control result that overrides state.""" """When device updates, clear control result that overrides state."""
self.control_result = None self.control_result = None
super()._update_callback() super()._update_callback()

View file

@ -1,6 +1,8 @@
"""Provides device triggers for Shelly.""" """Provides device triggers for Shelly."""
from __future__ import annotations from __future__ import annotations
from typing import Any, Final
import voluptuous as vol import voluptuous as vol
from homeassistant.components.automation import AutomationActionType from homeassistant.components.automation import AutomationActionType
@ -20,6 +22,7 @@ from homeassistant.const import (
from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import get_device_wrapper
from .const import ( from .const import (
ATTR_CHANNEL, ATTR_CHANNEL,
ATTR_CLICK_TYPE, ATTR_CLICK_TYPE,
@ -31,9 +34,9 @@ from .const import (
SHBTN_MODELS, SHBTN_MODELS,
SUPPORTED_INPUTS_EVENTS_TYPES, SUPPORTED_INPUTS_EVENTS_TYPES,
) )
from .utils import get_device_wrapper, get_input_triggers from .utils import get_input_triggers
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{ {
vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES), vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES),
vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES), vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES),
@ -41,7 +44,9 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
) )
async def async_validate_trigger_config(hass, config): async def async_validate_trigger_config(
hass: HomeAssistant, config: dict[str, Any]
) -> dict[str, Any]:
"""Validate config.""" """Validate config."""
config = TRIGGER_SCHEMA(config) config = TRIGGER_SCHEMA(config)
@ -62,7 +67,9 @@ async def async_validate_trigger_config(hass, config):
) )
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: async def async_get_triggers(
hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:
"""List device triggers for Shelly devices.""" """List device triggers for Shelly devices."""
triggers = [] triggers = []

View file

@ -4,31 +4,39 @@ from __future__ import annotations
import asyncio import asyncio
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from typing import Any, Callable from typing import Any, Callable, Final, cast
import aioshelly import aioshelly
import async_timeout import async_timeout
from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.sensor import ATTR_STATE_CLASS
from homeassistant.core import callback from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import ( from homeassistant.helpers import (
device_registry, device_registry,
entity, entity,
entity_registry, entity_registry,
update_coordinator, update_coordinator,
) )
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import StateType
from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper
from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, COAP, DATA_CONFIG_ENTRY, DOMAIN, REST from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
from .utils import async_remove_shelly_entity, get_entity_name from .utils import async_remove_shelly_entity, get_entity_name
_LOGGER = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
async def async_setup_entry_attribute_entities( async def async_setup_entry_attribute_entities(
hass, config_entry, async_add_entities, sensors, sensor_class hass: HomeAssistant,
): config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
sensors: dict[tuple[str, str], BlockAttributeDescription],
sensor_class: Callable,
) -> None:
"""Set up entities for attributes.""" """Set up entities for attributes."""
wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id config_entry.entry_id
@ -45,8 +53,12 @@ async def async_setup_entry_attribute_entities(
async def async_setup_block_attribute_entities( async def async_setup_block_attribute_entities(
hass, async_add_entities, wrapper, sensors, sensor_class hass: HomeAssistant,
): async_add_entities: AddEntitiesCallback,
wrapper: ShellyDeviceWrapper,
sensors: dict[tuple[str, str], BlockAttributeDescription],
sensor_class: Callable,
) -> None:
"""Set up entities for block attributes.""" """Set up entities for block attributes."""
blocks = [] blocks = []
@ -82,8 +94,13 @@ async def async_setup_block_attribute_entities(
async def async_restore_block_attribute_entities( async def async_restore_block_attribute_entities(
hass, config_entry, async_add_entities, wrapper, sensors, sensor_class hass: HomeAssistant,
): config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
wrapper: ShellyDeviceWrapper,
sensors: dict[tuple[str, str], BlockAttributeDescription],
sensor_class: Callable,
) -> None:
"""Restore block attributes entities.""" """Restore block attributes entities."""
entities = [] entities = []
@ -117,8 +134,12 @@ async def async_restore_block_attribute_entities(
async def async_setup_entry_rest( async def async_setup_entry_rest(
hass, config_entry, async_add_entities, sensors, sensor_class hass: HomeAssistant,
): config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
sensors: dict[str, RestAttributeDescription],
sensor_class: Callable,
) -> None:
"""Set up entities for REST sensors.""" """Set up entities for REST sensors."""
wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id config_entry.entry_id
@ -177,53 +198,53 @@ 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): def __init__(self, wrapper: ShellyDeviceWrapper, block: aioshelly.Block) -> None:
"""Initialize Shelly entity.""" """Initialize Shelly entity."""
self.wrapper = wrapper self.wrapper = wrapper
self.block = block self.block = block
self._name: str | None = get_entity_name(wrapper.device, block) self._name = get_entity_name(wrapper.device, block)
@property @property
def name(self): def name(self) -> str:
"""Name of entity.""" """Name of entity."""
return self._name return self._name
@property @property
def should_poll(self): def should_poll(self) -> bool:
"""If device should be polled.""" """If device should be polled."""
return False return False
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Device info.""" """Device info."""
return { return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
} }
@property @property
def available(self): def available(self) -> bool:
"""Available.""" """Available."""
return self.wrapper.last_update_success return self.wrapper.last_update_success
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return unique ID of entity.""" """Return unique ID of entity."""
return f"{self.wrapper.mac}-{self.block.description}" return f"{self.wrapper.mac}-{self.block.description}"
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""When entity is added to HASS.""" """When entity is added to HASS."""
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) self.async_on_remove(self.wrapper.async_add_listener(self._update_callback))
async def async_update(self): async def async_update(self) -> None:
"""Update entity with latest info.""" """Update entity with latest info."""
await self.wrapper.async_request_refresh() await self.wrapper.async_request_refresh()
@callback @callback
def _update_callback(self): def _update_callback(self) -> None:
"""Handle device update.""" """Handle device update."""
self.async_write_ha_state() self.async_write_ha_state()
async def set_state(self, **kwargs): async def set_state(self, **kwargs: Any) -> Any:
"""Set block state (HTTP request).""" """Set block state (HTTP request)."""
_LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) _LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs)
try: try:
@ -261,16 +282,16 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
unit = unit(block.info(attribute)) unit = unit(block.info(attribute))
self._unit: None | str | Callable[[dict], str] = unit self._unit: None | str | Callable[[dict], str] = unit
self._unique_id: None | str = f"{super().unique_id}-{self.attribute}" self._unique_id: str = f"{super().unique_id}-{self.attribute}"
self._name = get_entity_name(wrapper.device, block, self.description.name) self._name = get_entity_name(wrapper.device, block, self.description.name)
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return unique ID of entity.""" """Return unique ID of entity."""
return self._unique_id return self._unique_id
@property @property
def name(self): def name(self) -> str:
"""Name of sensor.""" """Name of sensor."""
return self._name return self._name
@ -280,27 +301,27 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
return self.description.default_enabled return self.description.default_enabled
@property @property
def attribute_value(self): def attribute_value(self) -> StateType:
"""Value of sensor.""" """Value of sensor."""
value = getattr(self.block, self.attribute) value = getattr(self.block, self.attribute)
if value is None: if value is None:
return None return None
return self.description.value(value) return cast(StateType, self.description.value(value))
@property @property
def device_class(self): def device_class(self) -> str | None:
"""Device class of sensor.""" """Device class of sensor."""
return self.description.device_class return self.description.device_class
@property @property
def icon(self): def icon(self) -> str | None:
"""Icon of sensor.""" """Icon of sensor."""
return self.description.icon return self.description.icon
@property @property
def available(self): def available(self) -> bool:
"""Available.""" """Available."""
available = super().available available = super().available
@ -310,7 +331,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
return self.description.available(self.block) return self.description.available(self.block)
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes.""" """Return the state attributes."""
if self.description.extra_state_attributes is None: if self.description.extra_state_attributes is None:
return None return None
@ -336,12 +357,12 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
self._last_value = None self._last_value = None
@property @property
def name(self): def name(self) -> str:
"""Name of sensor.""" """Name of sensor."""
return self._name return self._name
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Device info.""" """Device info."""
return { return {
"connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
@ -353,35 +374,36 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
return self.description.default_enabled return self.description.default_enabled
@property @property
def available(self): def available(self) -> bool:
"""Available.""" """Available."""
return self.wrapper.last_update_success return self.wrapper.last_update_success
@property @property
def attribute_value(self): def attribute_value(self) -> StateType:
"""Value of sensor.""" """Value of sensor."""
self._last_value = self.description.value( if callable(self.description.value):
self.wrapper.device.status, self._last_value self._last_value = self.description.value(
) self.wrapper.device.status, self._last_value
)
return self._last_value return self._last_value
@property @property
def device_class(self): def device_class(self) -> str | None:
"""Device class of sensor.""" """Device class of sensor."""
return self.description.device_class return self.description.device_class
@property @property
def icon(self): def icon(self) -> str | None:
"""Icon of sensor.""" """Icon of sensor."""
return self.description.icon return self.description.icon
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return unique ID of entity.""" """Return unique ID of entity."""
return f"{self.wrapper.mac}-{self.attribute}" return f"{self.wrapper.mac}-{self.attribute}"
@property @property
def extra_state_attributes(self) -> dict | None: def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes.""" """Return the state attributes."""
if self.description.extra_state_attributes is None: if self.description.extra_state_attributes is None:
return None return None
@ -400,11 +422,11 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
attribute: str, attribute: str,
description: BlockAttributeDescription, description: BlockAttributeDescription,
entry: entity_registry.RegistryEntry | None = None, entry: entity_registry.RegistryEntry | None = None,
sensors: set | None = None, sensors: dict[tuple[str, str], BlockAttributeDescription] | None = None,
) -> None: ) -> None:
"""Initialize the sleeping sensor.""" """Initialize the sleeping sensor."""
self.sensors = sensors self.sensors = sensors
self.last_state = None self.last_state: StateType = None
self.wrapper = wrapper self.wrapper = wrapper
self.attribute = attribute self.attribute = attribute
self.block = block self.block = block
@ -421,9 +443,9 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
) )
elif entry is not None: elif entry is not None:
self._unique_id = entry.unique_id self._unique_id = entry.unique_id
self._name = entry.original_name self._name = cast(str, entry.original_name)
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Handle entity which will be added.""" """Handle entity which will be added."""
await super().async_added_to_hass() await super().async_added_to_hass()
@ -434,7 +456,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
self.description.state_class = last_state.attributes.get(ATTR_STATE_CLASS) self.description.state_class = last_state.attributes.get(ATTR_STATE_CLASS)
@callback @callback
def _update_callback(self): def _update_callback(self) -> None:
"""Handle device update.""" """Handle device update."""
if ( if (
self.block is not None self.block is not None

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import Any from typing import Any, Final, cast
from aioshelly import Block from aioshelly import Block
import async_timeout import async_timeout
@ -23,7 +23,9 @@ from homeassistant.components.light import (
LightEntity, LightEntity,
brightness_supported, brightness_supported,
) )
from homeassistant.core import callback from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.color import ( from homeassistant.util.color import (
color_temperature_kelvin_to_mired, color_temperature_kelvin_to_mired,
color_temperature_mired_to_kelvin, color_temperature_mired_to_kelvin,
@ -44,10 +46,14 @@ from .const import (
from .entity import ShellyBlockEntity from .entity import ShellyBlockEntity
from .utils import async_remove_shelly_entity from .utils import async_remove_shelly_entity
_LOGGER = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up lights for device.""" """Set up lights for device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
@ -78,12 +84,12 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
"""Initialize light.""" """Initialize light."""
super().__init__(wrapper, block) super().__init__(wrapper, block)
self.control_result = None self.control_result: dict[str, Any] | None = None
self.mode_result = None self.mode_result: dict[str, Any] | None = None
self._supported_color_modes = set() self._supported_color_modes: set[str] = set()
self._supported_features = 0 self._supported_features: int = 0
self._min_kelvin = KELVIN_MIN_VALUE_WHITE self._min_kelvin: int = KELVIN_MIN_VALUE_WHITE
self._max_kelvin = KELVIN_MAX_VALUE self._max_kelvin: int = KELVIN_MAX_VALUE
if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"):
self._min_kelvin = KELVIN_MIN_VALUE_COLOR self._min_kelvin = KELVIN_MIN_VALUE_COLOR
@ -113,18 +119,18 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
def is_on(self) -> bool: def is_on(self) -> bool:
"""If light is on.""" """If light is on."""
if self.control_result: if self.control_result:
return self.control_result["ison"] return cast(bool, self.control_result["ison"])
return self.block.output return bool(self.block.output)
@property @property
def mode(self) -> str | None: def mode(self) -> str:
"""Return the color mode of the light.""" """Return the color mode of the light."""
if self.mode_result: if self.mode_result:
return self.mode_result["mode"] return cast(str, self.mode_result["mode"])
if hasattr(self.block, "mode"): if hasattr(self.block, "mode"):
return self.block.mode return cast(str, self.block.mode)
if ( if (
hasattr(self.block, "red") hasattr(self.block, "red")
@ -136,7 +142,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
return "white" return "white"
@property @property
def brightness(self) -> int | None: def brightness(self) -> int:
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
if self.mode == "color": if self.mode == "color":
if self.control_result: if self.control_result:
@ -152,7 +158,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
return round(255 * brightness_pct / 100) return round(255 * brightness_pct / 100)
@property @property
def color_mode(self) -> str | None: def color_mode(self) -> str:
"""Return the color mode of the light.""" """Return the color mode of the light."""
if self.mode == "color": if self.mode == "color":
if hasattr(self.block, "white"): if hasattr(self.block, "white"):
@ -191,7 +197,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
return (*self.rgb_color, white) return (*self.rgb_color, white)
@property @property
def color_temp(self) -> int | None: def color_temp(self) -> int:
"""Return the CT color value in mireds.""" """Return the CT color value in mireds."""
if self.control_result: if self.control_result:
color_temp = self.control_result["temp"] color_temp = self.control_result["temp"]
@ -244,7 +250,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
return STANDARD_RGB_EFFECTS[effect_index] return STANDARD_RGB_EFFECTS[effect_index]
async def async_turn_on(self, **kwargs) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on light.""" """Turn on light."""
if self.block.type == "relay": if self.block.type == "relay":
self.control_result = await self.set_state(turn="on") self.control_result = await self.set_state(turn="on")
@ -304,12 +310,12 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off light.""" """Turn off light."""
self.control_result = await self.set_state(turn="off") self.control_result = await self.set_state(turn="off")
self.async_write_ha_state() self.async_write_ha_state()
async def set_light_mode(self, set_mode): async def set_light_mode(self, set_mode: str | None) -> bool:
"""Change device mode color/white if mode has changed.""" """Change device mode color/white if mode has changed."""
if set_mode is None or self.mode == set_mode: if set_mode is None or self.mode == set_mode:
return True return True
@ -331,7 +337,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity):
return True return True
@callback @callback
def _update_callback(self): def _update_callback(self) -> None:
"""When device updates, clear control & mode result that overrides state.""" """When device updates, clear control & mode result that overrides state."""
self.control_result = None self.control_result = None
self.mode_result = None self.mode_result = None

View file

@ -1,8 +1,13 @@
"""Describe Shelly logbook events.""" """Describe Shelly logbook events."""
from __future__ import annotations
from typing import Callable
from homeassistant.const import ATTR_DEVICE_ID from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.typing import EventType
from . import get_device_wrapper
from .const import ( from .const import (
ATTR_CHANNEL, ATTR_CHANNEL,
ATTR_CLICK_TYPE, ATTR_CLICK_TYPE,
@ -10,15 +15,18 @@ from .const import (
DOMAIN, DOMAIN,
EVENT_SHELLY_CLICK, EVENT_SHELLY_CLICK,
) )
from .utils import get_device_name, get_device_wrapper from .utils import get_device_name
@callback @callback
def async_describe_events(hass, async_describe_event): def async_describe_events(
hass: HomeAssistant,
async_describe_event: Callable[[str, str, Callable[[EventType], dict]], None],
) -> None:
"""Describe logbook events.""" """Describe logbook events."""
@callback @callback
def async_describe_shelly_click_event(event): def async_describe_shelly_click_event(event: EventType) -> dict[str, str]:
"""Describe shelly.click logbook event.""" """Describe shelly.click logbook event."""
wrapper = get_device_wrapper(hass, event.data[ATTR_DEVICE_ID]) wrapper = get_device_wrapper(hass, event.data[ATTR_DEVICE_ID])
if wrapper: if wrapper:

View file

@ -1,6 +1,11 @@
"""Sensor for Shelly.""" """Sensor for Shelly."""
from __future__ import annotations
from typing import Final, cast
from homeassistant.components import sensor from homeassistant.components import sensor
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
DEGREE, DEGREE,
@ -12,6 +17,9 @@ from homeassistant.const import (
POWER_WATT, POWER_WATT,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
) )
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import SHAIR_MAX_WORK_HOURS from .const import SHAIR_MAX_WORK_HOURS
from .entity import ( from .entity import (
@ -25,7 +33,7 @@ from .entity import (
) )
from .utils import get_device_uptime, temperature_unit from .utils import get_device_uptime, temperature_unit
SENSORS = { SENSORS: Final = {
("device", "battery"): BlockAttributeDescription( ("device", "battery"): BlockAttributeDescription(
name="Battery", name="Battery",
unit=PERCENTAGE, unit=PERCENTAGE,
@ -153,7 +161,7 @@ SENSORS = {
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: block.extTemp != 999, available=lambda block: cast(bool, block.extTemp != 999),
), ),
("sensor", "humidity"): BlockAttributeDescription( ("sensor", "humidity"): BlockAttributeDescription(
name="Humidity", name="Humidity",
@ -161,7 +169,7 @@ SENSORS = {
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: block.extTemp != 999, available=lambda block: cast(bool, block.extTemp != 999),
), ),
("sensor", "luminosity"): BlockAttributeDescription( ("sensor", "luminosity"): BlockAttributeDescription(
name="Luminosity", name="Luminosity",
@ -199,7 +207,7 @@ SENSORS = {
), ),
} }
REST_SENSORS = { REST_SENSORS: Final = {
"rssi": RestAttributeDescription( "rssi": RestAttributeDescription(
name="RSSI", name="RSSI",
unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
@ -217,7 +225,11 @@ REST_SENSORS = {
} }
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensors for device.""" """Set up sensors for device."""
if config_entry.data["sleep_period"]: if config_entry.data["sleep_period"]:
await async_setup_entry_attribute_entities( await async_setup_entry_attribute_entities(
@ -236,36 +248,36 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity):
"""Represent a shelly sensor.""" """Represent a shelly sensor."""
@property @property
def state(self): def state(self) -> StateType:
"""Return value of sensor.""" """Return value of sensor."""
return self.attribute_value return self.attribute_value
@property @property
def state_class(self): def state_class(self) -> str | None:
"""State class of sensor.""" """State class of sensor."""
return self.description.state_class return self.description.state_class
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return unit of sensor.""" """Return unit of sensor."""
return self._unit return cast(str, self._unit)
class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity):
"""Represent a shelly REST sensor.""" """Represent a shelly REST sensor."""
@property @property
def state(self): def state(self) -> StateType:
"""Return value of sensor.""" """Return value of sensor."""
return self.attribute_value return self.attribute_value
@property @property
def state_class(self): def state_class(self) -> str | None:
"""State class of sensor.""" """State class of sensor."""
return self.description.state_class return self.description.state_class
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return unit of sensor.""" """Return unit of sensor."""
return self.description.unit return self.description.unit
@ -274,7 +286,7 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
"""Represent a shelly sleeping sensor.""" """Represent a shelly sleeping sensor."""
@property @property
def state(self): def state(self) -> StateType:
"""Return value of sensor.""" """Return value of sensor."""
if self.block is not None: if self.block is not None:
return self.attribute_value return self.attribute_value
@ -282,11 +294,11 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
return self.last_state return self.last_state
@property @property
def state_class(self): def state_class(self) -> str | None:
"""State class of sensor.""" """State class of sensor."""
return self.description.state_class return self.description.state_class
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return unit of sensor.""" """Return unit of sensor."""
return self._unit return cast(str, self._unit)

View file

@ -1,8 +1,14 @@
"""Switch for Shelly.""" """Switch for Shelly."""
from __future__ import annotations
from typing import Any, cast
from aioshelly import Block from aioshelly import Block
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ShellyDeviceWrapper from . import ShellyDeviceWrapper
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN
@ -10,7 +16,11 @@ from .entity import ShellyBlockEntity
from .utils import async_remove_shelly_entity from .utils import async_remove_shelly_entity
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up switches for device.""" """Set up switches for device."""
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
@ -50,28 +60,28 @@ class RelaySwitch(ShellyBlockEntity, SwitchEntity):
def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
"""Initialize relay switch.""" """Initialize relay switch."""
super().__init__(wrapper, block) super().__init__(wrapper, block)
self.control_result = None self.control_result: dict[str, Any] | None = None
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""If switch is on.""" """If switch is on."""
if self.control_result: if self.control_result:
return self.control_result["ison"] return cast(bool, self.control_result["ison"])
return self.block.output return bool(self.block.output)
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on relay.""" """Turn on relay."""
self.control_result = await self.set_state(turn="on") self.control_result = await self.set_state(turn="on")
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off relay.""" """Turn off relay."""
self.control_result = await self.set_state(turn="off") self.control_result = await self.set_state(turn="off")
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _update_callback(self): def _update_callback(self) -> None:
"""When device updates, clear control result that overrides state.""" """When device updates, clear control result that overrides state."""
self.control_result = None self.control_result = None
super()._update_callback() super()._update_callback()

View file

@ -3,19 +3,19 @@ from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import Any, Final, cast
import aioshelly import aioshelly
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
from homeassistant.helpers import singleton from homeassistant.helpers import singleton
from homeassistant.helpers.typing import EventType
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .const import ( from .const import (
BASIC_INPUTS_EVENTS_TYPES, BASIC_INPUTS_EVENTS_TYPES,
COAP,
CONF_COAP_PORT, CONF_COAP_PORT,
DATA_CONFIG_ENTRY,
DEFAULT_COAP_PORT, DEFAULT_COAP_PORT,
DOMAIN, DOMAIN,
SHBTN_INPUTS_EVENTS_TYPES, SHBTN_INPUTS_EVENTS_TYPES,
@ -24,10 +24,12 @@ from .const import (
UPTIME_DEVIATION, UPTIME_DEVIATION,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
async def async_remove_shelly_entity(hass, domain, unique_id): async def async_remove_shelly_entity(
hass: HomeAssistant, domain: str, unique_id: str
) -> None:
"""Remove a Shelly entity.""" """Remove a Shelly entity."""
entity_reg = await hass.helpers.entity_registry.async_get_registry() entity_reg = await hass.helpers.entity_registry.async_get_registry()
entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id) entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id)
@ -36,7 +38,7 @@ async def async_remove_shelly_entity(hass, domain, unique_id):
entity_reg.async_remove(entity_id) entity_reg.async_remove(entity_id)
def temperature_unit(block_info: dict) -> 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[aioshelly.BLOCK_VALUE_UNIT] == "F":
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
@ -45,7 +47,7 @@ def temperature_unit(block_info: dict) -> str:
def get_device_name(device: aioshelly.Device) -> str: def get_device_name(device: aioshelly.Device) -> str:
"""Naming for device.""" """Naming for device."""
return 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: aioshelly.Device, block: aioshelly.Block) -> int:
@ -96,7 +98,7 @@ def get_device_channel_name(
): ):
return entity_name return entity_name
channel_name = None channel_name: str | None = None
mode = block.type + "s" mode = 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")
@ -112,7 +114,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, block: aioshelly.Block) -> bool: def is_momentary_input(settings: dict[str, Any], block: aioshelly.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:
@ -134,7 +136,7 @@ def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool:
return button_type in ["momentary", "momentary_on_release"] return button_type in ["momentary", "momentary_on_release"]
def get_device_uptime(status: dict, last_uptime: str) -> str: def get_device_uptime(status: dict[str, Any], last_uptime: str) -> str:
"""Return device uptime string, tolerate up to 5 seconds deviation.""" """Return device uptime string, tolerate up to 5 seconds deviation."""
delta_uptime = utcnow() - timedelta(seconds=status["uptime"]) delta_uptime = utcnow() - timedelta(seconds=status["uptime"])
@ -178,22 +180,8 @@ def get_input_triggers(
return triggers return triggers
def get_device_wrapper(hass: HomeAssistant, device_id: str):
"""Get a Shelly device wrapper for the given device id."""
if not hass.data.get(DOMAIN):
return None
for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]:
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(COAP)
if wrapper and wrapper.device_id == device_id:
return wrapper
return None
@singleton.singleton("shelly_coap") @singleton.singleton("shelly_coap")
async def get_coap_context(hass): async def get_coap_context(hass: HomeAssistant) -> aioshelly.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 = aioshelly.COAP()
if DOMAIN in hass.data: if DOMAIN in hass.data:
@ -204,7 +192,7 @@ async def get_coap_context(hass):
await context.initialize(port) await context.initialize(port)
@callback @callback
def shutdown_listener(ev): def shutdown_listener(ev: EventType) -> None:
context.close() context.close()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
@ -212,7 +200,7 @@ async def get_coap_context(hass):
return context return context
def get_device_sleep_period(settings: dict) -> int: def get_device_sleep_period(settings: dict[str, Any]) -> int:
"""Return the device sleep period in seconds or 0 for non sleeping devices.""" """Return the device sleep period in seconds or 0 for non sleeping devices."""
sleep_period = 0 sleep_period = 0

View file

@ -825,6 +825,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.shelly.*]
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.slack.*] [mypy-homeassistant.components.slack.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true