Improve typing in Shelly integration (#52544)
This commit is contained in:
parent
f128bc9ef8
commit
772cbd59d7
14 changed files with 365 additions and 245 deletions
|
@ -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.*
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue