Type mysensors strictly (#51535)

This commit is contained in:
Martin Hjelmare 2021-06-07 16:04:04 +02:00 committed by GitHub
parent 564042ec67
commit 7560a77e0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 329 additions and 192 deletions

View File

@ -46,6 +46,7 @@ homeassistant.components.local_ip.*
homeassistant.components.lock.* homeassistant.components.lock.*
homeassistant.components.mailbox.* homeassistant.components.mailbox.*
homeassistant.components.media_player.* homeassistant.components.media_player.*
homeassistant.components.mysensors.*
homeassistant.components.nam.* homeassistant.components.nam.*
homeassistant.components.network.* homeassistant.components.network.*
homeassistant.components.notify.* homeassistant.components.notify.*

View File

@ -40,6 +40,7 @@ from .const import (
MYSENSORS_ON_UNLOAD, MYSENSORS_ON_UNLOAD,
PLATFORMS_WITH_ENTRY_SUPPORT, PLATFORMS_WITH_ENTRY_SUPPORT,
DevId, DevId,
DiscoveryInfo,
SensorType, SensorType,
) )
from .device import MySensorsDevice, get_mysensors_devices from .device import MySensorsDevice, get_mysensors_devices
@ -70,7 +71,7 @@ def set_default_persistence_file(value: dict) -> dict:
return value return value
def has_all_unique_files(value): def has_all_unique_files(value: list[dict]) -> list[dict]:
"""Validate that all persistence files are unique and set if any is set.""" """Validate that all persistence files are unique and set if any is set."""
persistence_files = [gateway[CONF_PERSISTENCE_FILE] for gateway in value] persistence_files = [gateway[CONF_PERSISTENCE_FILE] for gateway in value]
schema = vol.Schema(vol.Unique()) schema = vol.Schema(vol.Unique())
@ -78,17 +79,17 @@ def has_all_unique_files(value):
return value return value
def is_persistence_file(value): def is_persistence_file(value: str) -> str:
"""Validate that persistence file path ends in either .pickle or .json.""" """Validate that persistence file path ends in either .pickle or .json."""
if value.endswith((".json", ".pickle")): if value.endswith((".json", ".pickle")):
return value return value
raise vol.Invalid(f"{value} does not end in either `.json` or `.pickle`") raise vol.Invalid(f"{value} does not end in either `.json` or `.pickle`")
def deprecated(key): def deprecated(key: str) -> Callable[[dict], dict]:
"""Mark key as deprecated in configuration.""" """Mark key as deprecated in configuration."""
def validator(config): def validator(config: dict) -> dict:
"""Check if key is in config, log warning and remove key.""" """Check if key is in config, log warning and remove key."""
if key not in config: if key not in config:
return config return config
@ -270,7 +271,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def setup_mysensors_platform( def setup_mysensors_platform(
hass: HomeAssistant, hass: HomeAssistant,
domain: str, # hass platform name domain: str, # hass platform name
discovery_info: dict[str, list[DevId]], discovery_info: DiscoveryInfo,
device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsDevice]], device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsDevice]],
device_args: ( device_args: (
None | tuple None | tuple

View File

@ -1,4 +1,6 @@
"""Support for MySensors binary sensors.""" """Support for MySensors binary sensors."""
from __future__ import annotations
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOISTURE,
@ -17,6 +19,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DiscoveryInfo
from .helpers import on_unload from .helpers import on_unload
SENSORS = { SENSORS = {
@ -35,11 +38,11 @@ async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
): ) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway).""" """Set up this platform for a specific ConfigEntry(==Gateway)."""
@callback @callback
def async_discover(discovery_info): def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors binary_sensor.""" """Discover and add a MySensors binary_sensor."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, hass,
@ -64,12 +67,12 @@ class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorEntity
"""Representation of a MySensors Binary Sensor child node.""" """Representation of a MySensors Binary Sensor child node."""
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return True if the binary sensor is on.""" """Return True if the binary sensor is on."""
return self._values.get(self.value_type) == STATE_ON return self._values.get(self.value_type) == STATE_ON
@property @property
def device_class(self): def device_class(self) -> str | None:
"""Return the class of this sensor, from DEVICE_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
pres = self.gateway.const.Presentation pres = self.gateway.const.Presentation
device_class = SENSORS.get(pres(self.child_type).name) device_class = SENSORS.get(pres(self.child_type).name)

View File

@ -1,4 +1,8 @@
"""MySensors platform that offers a Climate (MySensors-HVAC) component.""" """MySensors platform that offers a Climate (MySensors-HVAC) component."""
from __future__ import annotations
from typing import Any
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -13,7 +17,7 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE_RANGE,
) )
from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -43,10 +47,10 @@ async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
): ) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway).""" """Set up this platform for a specific ConfigEntry(==Gateway)."""
async def async_discover(discovery_info): async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors climate.""" """Discover and add a MySensors climate."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, hass,
@ -71,7 +75,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
"""Representation of a MySensors HVAC.""" """Representation of a MySensors HVAC."""
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Return the list of supported features.""" """Return the list of supported features."""
features = 0 features = 0
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
@ -87,22 +91,23 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
return features return features
@property @property
def temperature_unit(self): def temperature_unit(self) -> str:
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
@property @property
def current_temperature(self): def current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""
value = self._values.get(self.gateway.const.SetReq.V_TEMP) value: str | None = self._values.get(self.gateway.const.SetReq.V_TEMP)
float_value: float | None = None
if value is not None: if value is not None:
value = float(value) float_value = float(value)
return value return float_value
@property @property
def target_temperature(self): def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if ( if (
@ -116,42 +121,46 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
return float(temp) if temp is not None else None return float(temp) if temp is not None else None
@property @property
def target_temperature_high(self): def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach.""" """Return the highbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_HEAT in self._values: if set_req.V_HVAC_SETPOINT_HEAT in self._values:
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
return float(temp) if temp is not None else None return float(temp) if temp is not None else None
return None
@property @property
def target_temperature_low(self): def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach.""" """Return the lowbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_COOL in self._values: if set_req.V_HVAC_SETPOINT_COOL in self._values:
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
return float(temp) if temp is not None else None return float(temp) if temp is not None else None
@property return None
def hvac_mode(self):
"""Return current operation ie. heat, cool, idle."""
return self._values.get(self.value_type)
@property @property
def hvac_modes(self): def hvac_mode(self) -> str:
"""Return current operation ie. heat, cool, idle."""
return self._values.get(self.value_type, HVAC_MODE_HEAT)
@property
def hvac_modes(self) -> list[str]:
"""List of available operation modes.""" """List of available operation modes."""
return OPERATION_LIST return OPERATION_LIST
@property @property
def fan_mode(self): def fan_mode(self) -> str | None:
"""Return the fan setting.""" """Return the fan setting."""
return self._values.get(self.gateway.const.SetReq.V_HVAC_SPEED) return self._values.get(self.gateway.const.SetReq.V_HVAC_SPEED)
@property @property
def fan_modes(self): def fan_modes(self) -> list[str]:
"""List of available fan modes.""" """List of available fan modes."""
return FAN_LIST return FAN_LIST
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
@ -183,7 +192,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
self._values[value_type] = value self._values[value_type] = value
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target temperature.""" """Set new target temperature."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(
@ -194,7 +203,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
self._values[set_req.V_HVAC_SPEED] = fan_mode self._values[set_req.V_HVAC_SPEED] = fan_mode
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target temperature.""" """Set new target temperature."""
self.gateway.set_child_value( self.gateway.set_child_value(
self.node_id, self.node_id,
@ -208,7 +217,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
self._values[self.value_type] = hvac_mode self._values[self.value_type] = hvac_mode
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self): async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
await super().async_update() await super().async_update()
self._values[self.value_type] = DICT_MYS_TO_HA[self._values[self.value_type]] self._values[self.value_type] = DICT_MYS_TO_HA[self._values[self.value_type]]

View File

@ -82,8 +82,8 @@ def _validate_version(version: str) -> dict[str, str]:
def _is_same_device( def _is_same_device(
gw_type: ConfGatewayType, user_input: dict[str, str], entry: ConfigEntry gw_type: ConfGatewayType, user_input: dict[str, Any], entry: ConfigEntry
): ) -> bool:
"""Check if another ConfigDevice is actually the same as user_input. """Check if another ConfigDevice is actually the same as user_input.
This function only compares addresses and tcp ports, so it is possible to fool it with tricks like port forwarding. This function only compares addresses and tcp ports, so it is possible to fool it with tricks like port forwarding.
@ -91,7 +91,9 @@ def _is_same_device(
if entry.data[CONF_DEVICE] != user_input[CONF_DEVICE]: if entry.data[CONF_DEVICE] != user_input[CONF_DEVICE]:
return False return False
if gw_type == CONF_GATEWAY_TYPE_TCP: if gw_type == CONF_GATEWAY_TYPE_TCP:
return entry.data[CONF_TCP_PORT] == user_input[CONF_TCP_PORT] entry_tcp_port: int = entry.data[CONF_TCP_PORT]
input_tcp_port: int = user_input[CONF_TCP_PORT]
return entry_tcp_port == input_tcp_port
if gw_type == CONF_GATEWAY_TYPE_MQTT: if gw_type == CONF_GATEWAY_TYPE_MQTT:
entry_topics = { entry_topics = {
entry.data[CONF_TOPIC_IN_PREFIX], entry.data[CONF_TOPIC_IN_PREFIX],

View File

@ -2,23 +2,23 @@
from __future__ import annotations from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from typing import Literal, Tuple from typing import Final, Literal, Tuple, TypedDict
ATTR_DEVICES: str = "devices" ATTR_DEVICES: Final = "devices"
ATTR_GATEWAY_ID: str = "gateway_id" ATTR_GATEWAY_ID: Final = "gateway_id"
CONF_BAUD_RATE: str = "baud_rate" CONF_BAUD_RATE: Final = "baud_rate"
CONF_DEVICE: str = "device" CONF_DEVICE: Final = "device"
CONF_GATEWAYS: str = "gateways" CONF_GATEWAYS: Final = "gateways"
CONF_NODES: str = "nodes" CONF_NODES: Final = "nodes"
CONF_PERSISTENCE: str = "persistence" CONF_PERSISTENCE: Final = "persistence"
CONF_PERSISTENCE_FILE: str = "persistence_file" CONF_PERSISTENCE_FILE: Final = "persistence_file"
CONF_RETAIN: str = "retain" CONF_RETAIN: Final = "retain"
CONF_TCP_PORT: str = "tcp_port" CONF_TCP_PORT: Final = "tcp_port"
CONF_TOPIC_IN_PREFIX: str = "topic_in_prefix" CONF_TOPIC_IN_PREFIX: Final = "topic_in_prefix"
CONF_TOPIC_OUT_PREFIX: str = "topic_out_prefix" CONF_TOPIC_OUT_PREFIX: Final = "topic_out_prefix"
CONF_VERSION: str = "version" CONF_VERSION: Final = "version"
CONF_GATEWAY_TYPE: str = "gateway_type" CONF_GATEWAY_TYPE: Final = "gateway_type"
ConfGatewayType = Literal["Serial", "TCP", "MQTT"] ConfGatewayType = Literal["Serial", "TCP", "MQTT"]
CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial" CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial"
CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP" CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP"
@ -29,19 +29,28 @@ CONF_GATEWAY_TYPE_ALL: list[str] = [
CONF_GATEWAY_TYPE_TCP, CONF_GATEWAY_TYPE_TCP,
] ]
DOMAIN: str = "mysensors" DOMAIN: Final = "mysensors"
MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}"
MYSENSORS_GATEWAYS: str = "mysensors_gateways" MYSENSORS_GATEWAYS: Final = "mysensors_gateways"
PLATFORM: str = "platform" PLATFORM: Final = "platform"
SCHEMA: str = "schema" SCHEMA: Final = "schema"
CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}" CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}"
NODE_CALLBACK: str = "mysensors_node_callback_{}_{}" NODE_CALLBACK: str = "mysensors_node_callback_{}_{}"
MYSENSORS_DISCOVERY = "mysensors_discovery_{}_{}" MYSENSORS_DISCOVERY: str = "mysensors_discovery_{}_{}"
MYSENSORS_ON_UNLOAD = "mysensors_on_unload_{}" MYSENSORS_ON_UNLOAD: str = "mysensors_on_unload_{}"
TYPE: str = "type" TYPE: Final = "type"
UPDATE_DELAY: float = 0.1 UPDATE_DELAY: float = 0.1
SERVICE_SEND_IR_CODE: str = "send_ir_code"
class DiscoveryInfo(TypedDict):
"""Represent the discovery info type for mysensors platforms."""
devices: list[DevId]
name: str # CONF_NAME is used in the notify base integration.
gateway_id: GatewayId
SERVICE_SEND_IR_CODE: Final = "send_ir_code"
SensorType = str SensorType = str
# S_DOOR, S_MOTION, S_SMOKE, ... # S_DOOR, S_MOTION, S_SMOKE, ...

View File

@ -1,10 +1,13 @@
"""Support for MySensors covers.""" """Support for MySensors covers."""
from __future__ import annotations
from enum import Enum, unique from enum import Enum, unique
import logging import logging
from typing import Any
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity
from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -30,10 +33,10 @@ async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
): ) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway).""" """Set up this platform for a specific ConfigEntry(==Gateway)."""
async def async_discover(discovery_info): async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors cover.""" """Discover and add a MySensors cover."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, hass,
@ -57,7 +60,7 @@ async def async_setup_entry(
class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
"""Representation of the value of a MySensors Cover child node.""" """Representation of the value of a MySensors Cover child node."""
def get_cover_state(self): def get_cover_state(self) -> CoverState:
"""Return a CoverState enum representing the state of the cover.""" """Return a CoverState enum representing the state of the cover."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
v_up = self._values.get(set_req.V_UP) == STATE_ON v_up = self._values.get(set_req.V_UP) == STATE_ON
@ -69,7 +72,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
# or V_STATUS. # or V_STATUS.
amount = 100 amount = 100
if set_req.V_DIMMER in self._values: if set_req.V_DIMMER in self._values:
amount = self._values.get(set_req.V_DIMMER) amount = self._values[set_req.V_DIMMER]
else: else:
amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0 amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0
@ -82,22 +85,22 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
return CoverState.OPEN return CoverState.OPEN
@property @property
def is_closed(self): def is_closed(self) -> bool:
"""Return True if the cover is closed.""" """Return True if the cover is closed."""
return self.get_cover_state() == CoverState.CLOSED return self.get_cover_state() == CoverState.CLOSED
@property @property
def is_closing(self): def is_closing(self) -> bool:
"""Return True if the cover is closing.""" """Return True if the cover is closing."""
return self.get_cover_state() == CoverState.CLOSING return self.get_cover_state() == CoverState.CLOSING
@property @property
def is_opening(self): def is_opening(self) -> bool:
"""Return True if the cover is opening.""" """Return True if the cover is opening."""
return self.get_cover_state() == CoverState.OPENING return self.get_cover_state() == CoverState.OPENING
@property @property
def current_cover_position(self): def current_cover_position(self) -> int | None:
"""Return current position of cover. """Return current position of cover.
None is unknown, 0 is closed, 100 is fully open. None is unknown, 0 is closed, 100 is fully open.
@ -105,7 +108,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_DIMMER) return self._values.get(set_req.V_DIMMER)
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs: Any) -> None:
"""Move the cover up.""" """Move the cover up."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(
@ -119,7 +122,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self._values[set_req.V_LIGHT] = STATE_ON self._values[set_req.V_LIGHT] = STATE_ON
self.async_write_ha_state() self.async_write_ha_state()
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs: Any) -> None:
"""Move the cover down.""" """Move the cover down."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(
@ -133,7 +136,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self._values[set_req.V_LIGHT] = STATE_OFF self._values[set_req.V_LIGHT] = STATE_OFF
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."""
position = kwargs.get(ATTR_POSITION) position = kwargs.get(ATTR_POSITION)
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
@ -145,7 +148,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self._values[set_req.V_DIMMER] = position self._values[set_req.V_DIMMER] = 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 device.""" """Stop the device."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(

View File

@ -66,10 +66,10 @@ class MySensorsDevice:
return self.gateway_id, self.node_id, self.child_id, self.value_type return self.gateway_id, self.node_id, self.child_id, self.value_type
@property @property
def _logger(self): def _logger(self) -> logging.Logger:
return logging.getLogger(f"{__name__}.{self.name}") return logging.getLogger(f"{__name__}.{self.name}")
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self) -> None:
"""Remove this entity from home assistant.""" """Remove this entity from home assistant."""
for platform in PLATFORM_TYPES: for platform in PLATFORM_TYPES:
platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform) platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform)
@ -91,17 +91,26 @@ class MySensorsDevice:
@property @property
def sketch_name(self) -> str: def sketch_name(self) -> str:
"""Return the name of the sketch running on the whole node (will be the same for several entities!).""" """Return the name of the sketch running on the whole node.
return self._node.sketch_name
The name will be the same for several entities.
"""
return self._node.sketch_name # type: ignore[no-any-return]
@property @property
def sketch_version(self) -> str: def sketch_version(self) -> str:
"""Return the version of the sketch running on the whole node (will be the same for several entities!).""" """Return the version of the sketch running on the whole node.
return self._node.sketch_version
The name will be the same for several entities.
"""
return self._node.sketch_version # type: ignore[no-any-return]
@property @property
def node_name(self) -> str: def node_name(self) -> str:
"""Name of the whole node (will be the same for several entities!).""" """Name of the whole node.
The name will be the same for several entities.
"""
return f"{self.sketch_name} {self.node_id}" return f"{self.sketch_name} {self.node_id}"
@property @property
@ -111,7 +120,7 @@ class MySensorsDevice:
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return a dict that allows home assistant to puzzle all entities belonging to a node together.""" """Return the device info."""
return { return {
"identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")}, "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")},
"name": self.node_name, "name": self.node_name,
@ -120,13 +129,13 @@ class MySensorsDevice:
} }
@property @property
def name(self): def name(self) -> str:
"""Return the name of this entity.""" """Return the name of this entity."""
return f"{self.node_name} {self.child_id}" return f"{self.node_name} {self.child_id}"
@property @property
def extra_state_attributes(self): def _extra_attributes(self) -> dict[str, Any]:
"""Return device specific state attributes.""" """Return device specific attributes."""
node = self.gateway.sensors[self.node_id] node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id] child = node.children[self.child_id]
attr = { attr = {
@ -136,10 +145,6 @@ class MySensorsDevice:
ATTR_DESCRIPTION: child.description, ATTR_DESCRIPTION: child.description,
ATTR_NODE_ID: self.node_id, ATTR_NODE_ID: self.node_id,
} }
# This works when we are actually an Entity (i.e. all platforms except device_tracker)
if hasattr(self, "platform"):
# pylint: disable=no-member
attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
@ -148,7 +153,7 @@ class MySensorsDevice:
return attr return attr
async def async_update(self): async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id] node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id] child = node.children[self.child_id]
@ -175,17 +180,17 @@ class MySensorsDevice:
else: else:
self._values[value_type] = value self._values[value_type] = value
async def _async_update_callback(self): async def _async_update_callback(self) -> None:
"""Update the device.""" """Update the device."""
raise NotImplementedError raise NotImplementedError
@callback @callback
def async_update_callback(self): def async_update_callback(self) -> None:
"""Update the device after delay.""" """Update the device after delay."""
if self._update_scheduled: if self._update_scheduled:
return return
async def update(): async def update() -> None:
"""Perform update.""" """Perform update."""
try: try:
await self._async_update_callback() await self._async_update_callback()
@ -199,31 +204,47 @@ class MySensorsDevice:
self.hass.loop.call_later(UPDATE_DELAY, delayed_update) self.hass.loop.call_later(UPDATE_DELAY, delayed_update)
def get_mysensors_devices(hass, domain: str) -> dict[DevId, MySensorsDevice]: def get_mysensors_devices(
hass: HomeAssistant, domain: str
) -> dict[DevId, MySensorsDevice]:
"""Return MySensors devices for a hass platform name.""" """Return MySensors devices for a hass platform name."""
if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]: if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]:
hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
return hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] devices: dict[DevId, MySensorsDevice] = hass.data[DOMAIN][
MYSENSORS_PLATFORM_DEVICES.format(domain)
]
return devices
class MySensorsEntity(MySensorsDevice, Entity): class MySensorsEntity(MySensorsDevice, Entity):
"""Representation of a MySensors entity.""" """Representation of a MySensors entity."""
@property @property
def should_poll(self): def should_poll(self) -> bool:
"""Return the polling state. The gateway pushes its states.""" """Return the polling state. The gateway pushes its states."""
return False return False
@property @property
def available(self): def available(self) -> bool:
"""Return true if entity is available.""" """Return true if entity is available."""
return self.value_type in self._values return self.value_type in self._values
async def _async_update_callback(self): @property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return entity specific state attributes."""
attr = self._extra_attributes
assert self.platform
assert self.platform.config_entry
attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
return attr
async def _async_update_callback(self) -> None:
"""Update the entity.""" """Update the entity."""
await self.async_update_ha_state(True) await self.async_update_ha_state(True)
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Register update callback.""" """Register update callback."""
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(

View File

@ -1,8 +1,16 @@
"""Support for tracking MySensors devices.""" """Support for tracking MySensors devices."""
from __future__ import annotations
from typing import Any, Callable
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker import DOMAIN
from homeassistant.components.mysensors import DevId from homeassistant.components.mysensors import DevId
from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId from homeassistant.components.mysensors.const import (
ATTR_GATEWAY_ID,
DiscoveryInfo,
GatewayId,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify from homeassistant.util import slugify
@ -11,8 +19,11 @@ from .helpers import on_unload
async def async_setup_scanner( async def async_setup_scanner(
hass: HomeAssistant, config, async_see, discovery_info=None hass: HomeAssistant,
): config: dict[str, Any],
async_see: Callable,
discovery_info: DiscoveryInfo | None = None,
) -> bool:
"""Set up the MySensors device scanner.""" """Set up the MySensors device scanner."""
if not discovery_info: if not discovery_info:
return False return False
@ -55,13 +66,13 @@ async def async_setup_scanner(
class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
"""Represent a MySensors scanner.""" """Represent a MySensors scanner."""
def __init__(self, hass: HomeAssistant, async_see, *args): def __init__(self, hass: HomeAssistant, async_see: Callable, *args: Any) -> None:
"""Set up instance.""" """Set up instance."""
super().__init__(*args) super().__init__(*args)
self.async_see = async_see self.async_see = async_see
self.hass = hass self.hass = hass
async def _async_update_callback(self): async def _async_update_callback(self) -> None:
"""Update the device.""" """Update the device."""
await self.async_update() await self.async_update()
node = self.gateway.sensors[self.node_id] node = self.gateway.sensors[self.node_id]
@ -74,5 +85,5 @@ class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
host_name=self.name, host_name=self.name,
gps=(latitude, longitude), gps=(latitude, longitude),
battery=node.battery_level, battery=node.battery_level,
attributes=self.extra_state_attributes, attributes=self._extra_attributes,
) )

View File

@ -14,6 +14,10 @@ from mysensors import BaseAsyncGateway, Message, Sensor, mysensors
import voluptuous as vol import voluptuous as vol
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
from homeassistant.components.mqtt.models import (
Message as MQTTMessage,
PublishPayloadType,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
@ -51,7 +55,7 @@ GATEWAY_READY_TIMEOUT = 20.0
MQTT_COMPONENT = "mqtt" MQTT_COMPONENT = "mqtt"
def is_serial_port(value): def is_serial_port(value: str) -> str:
"""Validate that value is a windows serial port or a unix device.""" """Validate that value is a windows serial port or a unix device."""
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
ports = (f"COM{idx + 1}" for idx in range(256)) ports = (f"COM{idx + 1}" for idx in range(256))
@ -61,7 +65,7 @@ def is_serial_port(value):
return cv.isdevice(value) return cv.isdevice(value)
def is_socket_address(value): def is_socket_address(value: str) -> str:
"""Validate that value is a valid address.""" """Validate that value is a valid address."""
try: try:
socket.getaddrinfo(value, None) socket.getaddrinfo(value, None)
@ -179,15 +183,17 @@ async def _get_gateway(
return None return None
mqtt = hass.components.mqtt mqtt = hass.components.mqtt
def pub_callback(topic, payload, qos, retain): def pub_callback(topic: str, payload: str, qos: int, retain: bool) -> None:
"""Call MQTT publish function.""" """Call MQTT publish function."""
mqtt.async_publish(topic, payload, qos, retain) mqtt.async_publish(topic, payload, qos, retain)
def sub_callback(topic, sub_cb, qos): def sub_callback(
topic: str, sub_cb: Callable[[str, PublishPayloadType, int], None], qos: int
) -> None:
"""Call MQTT subscribe function.""" """Call MQTT subscribe function."""
@callback @callback
def internal_callback(msg): def internal_callback(msg: MQTTMessage) -> None:
"""Call callback.""" """Call callback."""
sub_cb(msg.topic, msg.payload, msg.qos) sub_cb(msg.topic, msg.payload, msg.qos)
@ -234,7 +240,7 @@ async def _get_gateway(
async def finish_setup( async def finish_setup(
hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway
): ) -> None:
"""Load any persistent devices and platforms and start gateway.""" """Load any persistent devices and platforms and start gateway."""
discover_tasks = [] discover_tasks = []
start_tasks = [] start_tasks = []
@ -249,7 +255,7 @@ async def finish_setup(
async def _discover_persistent_devices( async def _discover_persistent_devices(
hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway
): ) -> None:
"""Discover platforms for devices loaded via persistence file.""" """Discover platforms for devices loaded via persistence file."""
new_devices = defaultdict(list) new_devices = defaultdict(list)
for node_id in gateway.sensors: for node_id in gateway.sensors:
@ -265,7 +271,9 @@ async def _discover_persistent_devices(
discover_mysensors_platform(hass, entry.entry_id, platform, dev_ids) discover_mysensors_platform(hass, entry.entry_id, platform, dev_ids)
async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): async def gw_stop(
hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway
) -> None:
"""Stop the gateway.""" """Stop the gateway."""
connect_task = hass.data[DOMAIN].pop( connect_task = hass.data[DOMAIN].pop(
MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id), None MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id), None
@ -275,11 +283,14 @@ async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway):
await gateway.stop() await gateway.stop()
async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway): async def _gw_start(
hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway
) -> None:
"""Start the gateway.""" """Start the gateway."""
gateway_ready = asyncio.Event() gateway_ready = asyncio.Event()
def gateway_connected(_: BaseAsyncGateway): def gateway_connected(_: BaseAsyncGateway) -> None:
"""Handle gateway connected."""
gateway_ready.set() gateway_ready.set()
gateway.on_conn_made = gateway_connected gateway.on_conn_made = gateway_connected
@ -290,7 +301,8 @@ async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncG
gateway.start() gateway.start()
) # store the connect task so it can be cancelled in gw_stop ) # store the connect task so it can be cancelled in gw_stop
async def stop_this_gw(_: Event): async def stop_this_gw(_: Event) -> None:
"""Stop the gateway."""
await gw_stop(hass, entry, gateway) await gw_stop(hass, entry, gateway)
on_unload( on_unload(
@ -319,7 +331,7 @@ def _gw_callback_factory(
"""Return a new callback for the gateway.""" """Return a new callback for the gateway."""
@callback @callback
def mysensors_callback(msg: Message): def mysensors_callback(msg: Message) -> None:
"""Handle messages from a MySensors gateway. """Handle messages from a MySensors gateway.
All MySenors messages are received here. All MySenors messages are received here.

View File

@ -68,7 +68,7 @@ async def handle_sketch_version(
@callback @callback
def _handle_child_update( def _handle_child_update(
hass: HomeAssistant, gateway_id: GatewayId, validated: dict[str, list[DevId]] hass: HomeAssistant, gateway_id: GatewayId, validated: dict[str, list[DevId]]
): ) -> None:
"""Handle a child update.""" """Handle a child update."""
signals: list[str] = [] signals: list[str] = []
@ -91,7 +91,9 @@ def _handle_child_update(
@callback @callback
def _handle_node_update(hass: HomeAssistant, gateway_id: GatewayId, msg: Message): def _handle_node_update(
hass: HomeAssistant, gateway_id: GatewayId, msg: Message
) -> None:
"""Handle a node update.""" """Handle a node update."""
signal = NODE_CALLBACK.format(gateway_id, msg.node_id) signal = NODE_CALLBACK.format(gateway_id, msg.node_id)
async_dispatcher_send(hass, signal) async_dispatcher_send(hass, signal)

View File

@ -117,7 +117,10 @@ def switch_ir_send_schema(
def get_child_schema( def get_child_schema(
gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType, schema gateway: BaseAsyncGateway,
child: ChildSensor,
value_type_name: ValueType,
schema: dict,
) -> vol.Schema: ) -> vol.Schema:
"""Return a child schema.""" """Return a child schema."""
set_req = gateway.const.SetReq set_req = gateway.const.SetReq
@ -136,7 +139,7 @@ def get_child_schema(
def invalid_msg( def invalid_msg(
gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
): ) -> str:
"""Return a message for an invalid child during schema validation.""" """Return a message for an invalid child during schema validation."""
pres = gateway.const.Presentation pres = gateway.const.Presentation
set_req = gateway.const.SetReq set_req = gateway.const.SetReq

View File

@ -1,4 +1,8 @@
"""Support for MySensors lights.""" """Support for MySensors lights."""
from __future__ import annotations
from typing import Any
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -10,7 +14,6 @@ from homeassistant.components.light import (
SUPPORT_WHITE_VALUE, SUPPORT_WHITE_VALUE,
LightEntity, LightEntity,
) )
from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -19,6 +22,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.util.color import rgb_hex_to_rgb_list from homeassistant.util.color import rgb_hex_to_rgb_list
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType
from .device import MySensorsDevice
from .helpers import on_unload from .helpers import on_unload
SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE
@ -28,15 +33,15 @@ async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
): ) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway).""" """Set up this platform for a specific ConfigEntry(==Gateway)."""
device_class_map = { device_class_map: dict[SensorType, type[MySensorsDevice]] = {
"S_DIMMER": MySensorsLightDimmer, "S_DIMMER": MySensorsLightDimmer,
"S_RGB_LIGHT": MySensorsLightRGB, "S_RGB_LIGHT": MySensorsLightRGB,
"S_RGBW_LIGHT": MySensorsLightRGBW, "S_RGBW_LIGHT": MySensorsLightRGBW,
} }
async def async_discover(discovery_info): async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors light.""" """Discover and add a MySensors light."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, hass,
@ -60,35 +65,35 @@ async def async_setup_entry(
class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
"""Representation of a MySensors Light child node.""" """Representation of a MySensors Light child node."""
def __init__(self, *args): def __init__(self, *args: Any) -> None:
"""Initialize a MySensors Light.""" """Initialize a MySensors Light."""
super().__init__(*args) super().__init__(*args)
self._state = None self._state: bool | None = None
self._brightness = None self._brightness: int | None = None
self._hs = None self._hs: tuple[int, int] | None = None
self._white = None self._white: int | None = None
@property @property
def brightness(self): def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return self._brightness return self._brightness
@property @property
def hs_color(self): def hs_color(self) -> tuple[int, int] | None:
"""Return the hs color value [int, int].""" """Return the hs color value [int, int]."""
return self._hs return self._hs
@property @property
def white_value(self): def white_value(self) -> int | None:
"""Return the white value of this light between 0..255.""" """Return the white value of this light between 0..255."""
return self._white return self._white
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if device is on.""" """Return true if device is on."""
return self._state return bool(self._state)
def _turn_on_light(self): def _turn_on_light(self) -> None:
"""Turn on light child device.""" """Turn on light child device."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
@ -103,10 +108,9 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self._state = True self._state = True
self._values[set_req.V_LIGHT] = STATE_ON self._values[set_req.V_LIGHT] = STATE_ON
def _turn_on_dimmer(self, **kwargs): def _turn_on_dimmer(self, **kwargs: Any) -> None:
"""Turn on dimmer child device.""" """Turn on dimmer child device."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
brightness = self._brightness
if ( if (
ATTR_BRIGHTNESS not in kwargs ATTR_BRIGHTNESS not in kwargs
@ -114,7 +118,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
or set_req.V_DIMMER not in self._values or set_req.V_DIMMER not in self._values
): ):
return return
brightness = kwargs[ATTR_BRIGHTNESS] brightness: int = kwargs[ATTR_BRIGHTNESS]
percent = round(100 * brightness / 255) percent = round(100 * brightness / 255)
self.gateway.set_child_value( self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1 self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1
@ -125,17 +129,20 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self._brightness = brightness self._brightness = brightness
self._values[set_req.V_DIMMER] = percent self._values[set_req.V_DIMMER] = percent
def _turn_on_rgb_and_w(self, hex_template, **kwargs): def _turn_on_rgb_and_w(self, hex_template: str, **kwargs: Any) -> None:
"""Turn on RGB or RGBW child device.""" """Turn on RGB or RGBW child device."""
assert self._hs
assert self._white is not None
rgb = list(color_util.color_hs_to_RGB(*self._hs)) rgb = list(color_util.color_hs_to_RGB(*self._hs))
white = self._white white = self._white
hex_color = self._values.get(self.value_type) hex_color = self._values.get(self.value_type)
hs_color = kwargs.get(ATTR_HS_COLOR) hs_color: tuple[float, float] | None = kwargs.get(ATTR_HS_COLOR)
new_rgb: tuple[int, int, int] | None
if hs_color is not None: if hs_color is not None:
new_rgb = color_util.color_hs_to_RGB(*hs_color) new_rgb = color_util.color_hs_to_RGB(*hs_color)
else: else:
new_rgb = None new_rgb = None
new_white = kwargs.get(ATTR_WHITE_VALUE) new_white: int | None = kwargs.get(ATTR_WHITE_VALUE)
if new_rgb is None and new_white is None: if new_rgb is None and new_white is None:
return return
@ -155,11 +162,11 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
if self.assumed_state: if self.assumed_state:
# optimistically assume that light has changed state # optimistically assume that light has changed state
self._hs = color_util.color_RGB_to_hs(*rgb) self._hs = color_util.color_RGB_to_hs(*rgb) # type: ignore[assignment]
self._white = white self._white = white
self._values[self.value_type] = hex_color self._values[self.value_type] = hex_color
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
value_type = self.gateway.const.SetReq.V_LIGHT value_type = self.gateway.const.SetReq.V_LIGHT
self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1) self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1)
@ -170,13 +177,13 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _async_update_light(self): def _async_update_light(self) -> None:
"""Update the controller with values from light child.""" """Update the controller with values from light child."""
value_type = self.gateway.const.SetReq.V_LIGHT value_type = self.gateway.const.SetReq.V_LIGHT
self._state = self._values[value_type] == STATE_ON self._state = self._values[value_type] == STATE_ON
@callback @callback
def _async_update_dimmer(self): def _async_update_dimmer(self) -> None:
"""Update the controller with values from dimmer child.""" """Update the controller with values from dimmer child."""
value_type = self.gateway.const.SetReq.V_DIMMER value_type = self.gateway.const.SetReq.V_DIMMER
if value_type in self._values: if value_type in self._values:
@ -185,31 +192,31 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self._state = False self._state = False
@callback @callback
def _async_update_rgb_or_w(self): def _async_update_rgb_or_w(self) -> None:
"""Update the controller with values from RGB or RGBW child.""" """Update the controller with values from RGB or RGBW child."""
value = self._values[self.value_type] value = self._values[self.value_type]
color_list = rgb_hex_to_rgb_list(value) color_list = rgb_hex_to_rgb_list(value)
if len(color_list) > 3: if len(color_list) > 3:
self._white = color_list.pop() self._white = color_list.pop()
self._hs = color_util.color_RGB_to_hs(*color_list) self._hs = color_util.color_RGB_to_hs(*color_list) # type: ignore[assignment]
class MySensorsLightDimmer(MySensorsLight): class MySensorsLightDimmer(MySensorsLight):
"""Dimmer child class to MySensorsLight.""" """Dimmer child class to MySensorsLight."""
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
self._turn_on_light() self._turn_on_light()
self._turn_on_dimmer(**kwargs) self._turn_on_dimmer(**kwargs)
if self.assumed_state: if self.assumed_state:
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self): async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
await super().async_update() await super().async_update()
self._async_update_light() self._async_update_light()
@ -220,14 +227,14 @@ class MySensorsLightRGB(MySensorsLight):
"""RGB child class to MySensorsLight.""" """RGB child class to MySensorsLight."""
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if set_req.V_DIMMER in self._values: if set_req.V_DIMMER in self._values:
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
return SUPPORT_COLOR return SUPPORT_COLOR
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
self._turn_on_light() self._turn_on_light()
self._turn_on_dimmer(**kwargs) self._turn_on_dimmer(**kwargs)
@ -235,7 +242,7 @@ class MySensorsLightRGB(MySensorsLight):
if self.assumed_state: if self.assumed_state:
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self): async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
await super().async_update() await super().async_update()
self._async_update_light() self._async_update_light()
@ -247,14 +254,14 @@ class MySensorsLightRGBW(MySensorsLightRGB):
"""RGBW child class to MySensorsLightRGB.""" """RGBW child class to MySensorsLightRGB."""
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if set_req.V_DIMMER in self._values: if set_req.V_DIMMER in self._values:
return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW
return SUPPORT_MYSENSORS_RGBW return SUPPORT_MYSENSORS_RGBW
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
self._turn_on_light() self._turn_on_light()
self._turn_on_dimmer(**kwargs) self._turn_on_dimmer(**kwargs)

View File

@ -1,9 +1,20 @@
"""MySensors notification service.""" """MySensors notification service."""
from __future__ import annotations
from typing import Any
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificationService from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificationService
from homeassistant.core import HomeAssistant
from .const import DevId, DiscoveryInfo
async def async_get_service(hass, config, discovery_info=None): async def async_get_service(
hass: HomeAssistant,
config: dict[str, Any],
discovery_info: DiscoveryInfo | None = None,
) -> BaseNotificationService | None:
"""Get the MySensors notification service.""" """Get the MySensors notification service."""
if not discovery_info: if not discovery_info:
return None return None
@ -19,7 +30,7 @@ async def async_get_service(hass, config, discovery_info=None):
class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): class MySensorsNotificationDevice(mysensors.device.MySensorsDevice):
"""Represent a MySensors Notification device.""" """Represent a MySensors Notification device."""
def send_msg(self, msg): def send_msg(self, msg: str) -> None:
"""Send a message.""" """Send a message."""
for sub_msg in [msg[i : i + 25] for i in range(0, len(msg), 25)]: for sub_msg in [msg[i : i + 25] for i in range(0, len(msg), 25)]:
# Max mysensors payload is 25 bytes. # Max mysensors payload is 25 bytes.
@ -27,7 +38,7 @@ class MySensorsNotificationDevice(mysensors.device.MySensorsDevice):
self.node_id, self.child_id, self.value_type, sub_msg self.node_id, self.child_id, self.value_type, sub_msg
) )
def __repr__(self): def __repr__(self) -> str:
"""Return the representation.""" """Return the representation."""
return f"<MySensorsNotificationDevice {self.name}>" return f"<MySensorsNotificationDevice {self.name}>"
@ -35,11 +46,15 @@ class MySensorsNotificationDevice(mysensors.device.MySensorsDevice):
class MySensorsNotificationService(BaseNotificationService): class MySensorsNotificationService(BaseNotificationService):
"""Implement a MySensors notification service.""" """Implement a MySensors notification service."""
def __init__(self, hass): def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the service.""" """Initialize the service."""
self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) self.devices: dict[
DevId, MySensorsNotificationDevice
] = mysensors.get_mysensors_devices(
hass, DOMAIN
) # type: ignore[assignment]
async def async_send_message(self, message="", **kwargs): async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to a user.""" """Send a message to a user."""
target_devices = kwargs.get(ATTR_TARGET) target_devices = kwargs.get(ATTR_TARGET)
devices = [ devices = [

View File

@ -1,8 +1,9 @@
"""Support for MySensors sensors.""" """Support for MySensors sensors."""
from __future__ import annotations
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.components.sensor import DOMAIN, SensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -26,9 +27,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
from .helpers import on_unload from .helpers import on_unload
SENSORS = { SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = {
"V_TEMP": [None, "mdi:thermometer"], "V_TEMP": [None, "mdi:thermometer"],
"V_HUM": [PERCENTAGE, "mdi:water-percent"], "V_HUM": [PERCENTAGE, "mdi:water-percent"],
"V_DIMMER": [PERCENTAGE, "mdi:percent"], "V_DIMMER": [PERCENTAGE, "mdi:percent"],
@ -67,10 +69,10 @@ async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
): ) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway).""" """Set up this platform for a specific ConfigEntry(==Gateway)."""
async def async_discover(discovery_info): async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors sensor.""" """Discover and add a MySensors sensor."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, hass,
@ -95,7 +97,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity):
"""Representation of a MySensors Sensor child node.""" """Representation of a MySensors Sensor child node."""
@property @property
def force_update(self): def force_update(self) -> bool:
"""Return True if state updates should be forced. """Return True if state updates should be forced.
If True, a state change will be triggered anytime the state property is If True, a state change will be triggered anytime the state property is
@ -104,36 +106,43 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity):
return True return True
@property @property
def state(self): def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
return self._values.get(self.value_type) return self._values.get(self.value_type)
@property @property
def icon(self): def icon(self) -> str | None:
"""Return the icon to use in the frontend, if any.""" """Return the icon to use in the frontend, if any."""
icon = self._get_sensor_type()[1] icon = self._get_sensor_type()[1]
return icon return icon
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity.""" """Return the unit of measurement of this entity."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if ( if (
AwesomeVersion(self.gateway.protocol_version) >= AwesomeVersion("1.5") AwesomeVersion(self.gateway.protocol_version) >= AwesomeVersion("1.5")
and set_req.V_UNIT_PREFIX in self._values and set_req.V_UNIT_PREFIX in self._values
): ):
return self._values[set_req.V_UNIT_PREFIX] custom_unit: str = self._values[set_req.V_UNIT_PREFIX]
return custom_unit
if set_req(self.value_type) == set_req.V_TEMP:
if self.hass.config.units.is_metric:
return TEMP_CELSIUS
return TEMP_FAHRENHEIT
unit = self._get_sensor_type()[0] unit = self._get_sensor_type()[0]
return unit return unit
def _get_sensor_type(self): def _get_sensor_type(self) -> list[str | None]:
"""Return list with unit and icon of sensor type.""" """Return list with unit and icon of sensor type."""
pres = self.gateway.const.Presentation pres = self.gateway.const.Presentation
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
SENSORS[set_req.V_TEMP.name][0] = (
TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT _sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None])
) if isinstance(_sensor_type, dict):
sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) sensor_type = _sensor_type.get(pres(self.child_type).name, [None, None])
if isinstance(sensor_type, dict): else:
sensor_type = sensor_type.get(pres(self.child_type).name, [None, None]) sensor_type = _sensor_type
return sensor_type return sensor_type

View File

@ -1,16 +1,28 @@
"""Support for MySensors switches.""" """Support for MySensors switches."""
from __future__ import annotations
from contextlib import suppress
from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.components.switch import DOMAIN, SwitchEntity
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ...config_entries import ConfigEntry from ...config_entries import ConfigEntry
from ...helpers.dispatcher import async_dispatcher_connect from ...helpers.dispatcher import async_dispatcher_connect
from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE from .const import (
DOMAIN as MYSENSORS_DOMAIN,
MYSENSORS_DISCOVERY,
SERVICE_SEND_IR_CODE,
DiscoveryInfo,
SensorType,
)
from .device import MySensorsDevice
from .helpers import on_unload from .helpers import on_unload
ATTR_IR_CODE = "V_IR_SEND" ATTR_IR_CODE = "V_IR_SEND"
@ -24,9 +36,9 @@ async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
): ) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway).""" """Set up this platform for a specific ConfigEntry(==Gateway)."""
device_class_map = { device_class_map: dict[SensorType, type[MySensorsDevice]] = {
"S_DOOR": MySensorsSwitch, "S_DOOR": MySensorsSwitch,
"S_MOTION": MySensorsSwitch, "S_MOTION": MySensorsSwitch,
"S_SMOKE": MySensorsSwitch, "S_SMOKE": MySensorsSwitch,
@ -42,7 +54,7 @@ async def async_setup_entry(
"S_WATER_QUALITY": MySensorsSwitch, "S_WATER_QUALITY": MySensorsSwitch,
} }
async def async_discover(discovery_info): async def async_discover(discovery_info: DiscoveryInfo) -> None:
"""Discover and add a MySensors switch.""" """Discover and add a MySensors switch."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, hass,
@ -52,7 +64,7 @@ async def async_setup_entry(
async_add_entities=async_add_entities, async_add_entities=async_add_entities,
) )
async def async_send_ir_code_service(service): async def async_send_ir_code_service(service: ServiceCall) -> None:
"""Set IR code as device state attribute.""" """Set IR code as device state attribute."""
entity_ids = service.data.get(ATTR_ENTITY_ID) entity_ids = service.data.get(ATTR_ENTITY_ID)
ir_code = service.data.get(ATTR_IR_CODE) ir_code = service.data.get(ATTR_IR_CODE)
@ -98,17 +110,23 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
"""Representation of the value of a MySensors Switch child node.""" """Representation of the value of a MySensors Switch child node."""
@property @property
def current_power_w(self): def current_power_w(self) -> float | None:
"""Return the current power usage in W.""" """Return the current power usage in W."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_WATT) value = self._values.get(set_req.V_WATT)
float_value: float | None = None
if value is not None:
with suppress(ValueError):
float_value = float(value)
return float_value
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return True if switch is on.""" """Return True if switch is on."""
return self._values.get(self.value_type) == STATE_ON return self._values.get(self.value_type) == STATE_ON
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on.""" """Turn the switch on."""
self.gateway.set_child_value( self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 1, ack=1 self.node_id, self.child_id, self.value_type, 1, ack=1
@ -118,7 +136,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
self._values[self.value_type] = STATE_ON self._values[self.value_type] = STATE_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 the switch off.""" """Turn the switch off."""
self.gateway.set_child_value( self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 0, ack=1 self.node_id, self.child_id, self.value_type, 0, ack=1
@ -132,18 +150,18 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
class MySensorsIRSwitch(MySensorsSwitch): class MySensorsIRSwitch(MySensorsSwitch):
"""IR switch child class to MySensorsSwitch.""" """IR switch child class to MySensorsSwitch."""
def __init__(self, *args): def __init__(self, *args: Any) -> None:
"""Set up instance attributes.""" """Set up instance attributes."""
super().__init__(*args) super().__init__(*args)
self._ir_code = None self._ir_code: str | None = None
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return True if switch is on.""" """Return True if switch is on."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_LIGHT) == STATE_ON return self._values.get(set_req.V_LIGHT) == STATE_ON
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the IR switch on.""" """Turn the IR switch on."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
if ATTR_IR_CODE in kwargs: if ATTR_IR_CODE in kwargs:
@ -162,7 +180,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
# Turn off switch after switch was turned on # Turn off switch after switch was turned on
await self.async_turn_off() await self.async_turn_off()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the IR switch off.""" """Turn the IR switch off."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(
@ -173,7 +191,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
self._values[set_req.V_LIGHT] = STATE_OFF self._values[set_req.V_LIGHT] = STATE_OFF
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self): async def async_update(self) -> None:
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
await super().async_update() await super().async_update()
self._ir_code = self._values.get(self.value_type) self._ir_code = self._values.get(self.value_type)

View File

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