diff --git a/custom_components/changelog.txt b/custom_components/changelog.txt new file mode 100644 index 00000000..64bc9fcd --- /dev/null +++ b/custom_components/changelog.txt @@ -0,0 +1,11 @@ +Version 0.0.17 (2021-12-05) +- Add translation for HmIP-SRH states + +- Code quality: + - Use Enums from HaHomematic + - Add more type hints (fix mypy errors) + - Use assignment expressions + - Move services to own file + +Version 0.0.16 (2021-12-02) +- Initial release \ No newline at end of file diff --git a/custom_components/hahm/__init__.py b/custom_components/hahm/__init__.py index 22ff61cd..fec5464d 100644 --- a/custom_components/hahm/__init__.py +++ b/custom_components/hahm/__init__.py @@ -1,271 +1,56 @@ """ -hahomematic is a Python 3 (>= 3.6) module for Home Assistant to interact with +hahomematic is a Python 3 module for Home Assistant to interact with HomeMatic and homematic IP devices. -Some other devices (f.ex. Bosch, Intertechno) might be supported as well. https://github.com/danielperna84/hahomematic """ from __future__ import annotations -from datetime import datetime import logging -from hahomematic.const import ( - ATTR_ADDRESS, - ATTR_INTERFACE_ID, - ATTR_NAME, - ATTR_PARAMETER, - ATTR_VALUE, - HA_PLATFORMS, -) -from hahomematic.entity import GenericEntity -from hahomematic.hub import HmHub -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, ATTR_TIME from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from .const import ( - ATTR_CHANNEL, - ATTR_INSTANCE_NAME, - ATTR_INTERFACE, - ATTR_PARAM, - ATTR_PARAMSET, - ATTR_PARAMSET_KEY, - ATTR_RX_MODE, - ATTR_VALUE_TYPE, + CONF_ENABLE_SENSORS_FOR_SYSTEM_VARIABLES, + CONF_ENABLE_VIRTUAL_CHANNELS, DOMAIN, - SERVICE_PUT_PARAMSET, - SERVICE_SET_DEVICE_VALUE, - SERVICE_SET_INSTALL_MODE, - SERVICE_SET_VARIABLE_VALUE, - SERVICE_VIRTUAL_KEY, + HAHM_PLATFORMS, ) -from .controlunit import ControlUnit +from .control_unit import ControlConfig, ControlUnit +from .services import async_setup_services _LOGGER = logging.getLogger(__name__) -SCHEMA_SERVICE_VIRTUALKEY = vol.Schema( - { - vol.Optional(ATTR_INTERFACE_ID): cv.string, - vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_PARAMETER): cv.string, - } -) - -SCHEMA_SERVICE_SET_VARIABLE_VALUE = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.string, - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_VALUE): cv.match_all, - } -) - -SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema( - { - vol.Required(ATTR_INTERFACE_ID): cv.string, - vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_PARAMETER): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_VALUE): cv.match_all, - vol.Optional(ATTR_VALUE_TYPE): vol.In( - ["boolean", "dateTime.iso8601", "double", "int", "string"] - ), - vol.Optional(ATTR_INTERFACE_ID): cv.string, - } -) - -SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema( - { - vol.Required(ATTR_INTERFACE_ID): cv.string, - vol.Optional(ATTR_TIME, default=60): cv.positive_int, - vol.Optional(ATTR_MODE, default=1): vol.All(vol.Coerce(int), vol.In([1, 2])), - vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), - } -) - -SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema( - { - vol.Required(ATTR_INTERFACE_ID): cv.string, - vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_PARAMSET_KEY): vol.All(cv.string, vol.Upper), - vol.Required(ATTR_PARAMSET): dict, - vol.Optional(ATTR_RX_MODE): vol.All(cv.string, vol.Upper), - } -) - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up HA-Homematic from a config entry.""" - control_unit = ControlUnit(hass, entry=entry) + control_unit = ControlConfig( + hass=hass, + entry_id=config_entry.entry_id, + data=config_entry.data, + enable_virtual_channels=config_entry.options.get( + CONF_ENABLE_VIRTUAL_CHANNELS, False + ), + enable_sensors_for_system_variables=config_entry.options.get( + CONF_ENABLE_SENSORS_FOR_SYSTEM_VARIABLES, False + ), + ).get_control_unit() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = control_unit - hass.config_entries.async_setup_platforms(entry, HA_PLATFORMS) + hass.data[DOMAIN][config_entry.entry_id] = control_unit + hass.config_entries.async_setup_platforms(config_entry, HAHM_PLATFORMS) await control_unit.start() await async_setup_services(hass) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - control_unit = hass.data[DOMAIN][entry.entry_id] + control_unit = hass.data[DOMAIN][config_entry.entry_id] await control_unit.stop() control_unit.central.clear_all() - unload_ok = await hass.config_entries.async_unload_platforms(entry, HA_PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) + if unload_ok := await hass.config_entries.async_unload_platforms( + config_entry, HAHM_PLATFORMS + ): + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok - - -async def async_setup_services(hass: HomeAssistant) -> None: - """Setup servives""" - - async def _service_virtualkey(service): - """Service to handle virtualkey servicecalls.""" - interface_id = service.data[ATTR_INTERFACE_ID] - address = service.data[ATTR_ADDRESS] - parameter = service.data[ATTR_PARAMETER] - - control_unit = _get_cu_by_interface_id(hass, interface_id) - await control_unit.central.press_virtual_remote_key(address, parameter) - - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_VIRTUAL_KEY, - service_func=_service_virtualkey, - schema=SCHEMA_SERVICE_VIRTUALKEY, - ) - - async def _service_set_variable_value(service): - """Service to call setValue method for HomeMatic system variable.""" - entity_id = service.data.get(ATTR_ENTITY_ID) - name = service.data[ATTR_NAME] - value = service.data[ATTR_VALUE] - - hub = _get_hub_by_entity_id(hass, entity_id) - if hub: - await hub.set_variable(name, value) - - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_SET_VARIABLE_VALUE, - service_func=_service_set_variable_value, - schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE, - ) - - async def _service_set_device_value(service): - """Service to call setValue method for HomeMatic devices.""" - interface_id = service.data[ATTR_INTERFACE_ID] - address = service.data[ATTR_ADDRESS] - parameter = service.data[ATTR_PARAMETER] - value = service.data[ATTR_VALUE] - value_type = service.data.get(ATTR_VALUE_TYPE) - - # Convert value into correct XML-RPC Type. - # https://docs.python.org/3/library/xmlrpc.client.html#xmlrpc.client.ServerProxy - if value_type: - if value_type == "int": - value = int(value) - elif value_type == "double": - value = float(value) - elif value_type == "boolean": - value = bool(value) - elif value_type == "dateTime.iso8601": - value = datetime.strptime(value, "%Y%m%dT%H:%M:%S") - else: - # Default is 'string' - value = str(value) - - # Device not found - hm_entity = _get_hm_entity(hass, interface_id, address, parameter) - if hm_entity is None: - _LOGGER.error("%s not found!", address) - return - - await hm_entity.send_value(value) - - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_SET_DEVICE_VALUE, - service_func=_service_set_device_value, - schema=SCHEMA_SERVICE_SET_DEVICE_VALUE, - ) - - async def _service_set_install_mode(service): - """Service to set interface_id into install mode.""" - interface_id = service.data[ATTR_INTERFACE_ID] - mode = service.data.get(ATTR_MODE) - time = service.data.get(ATTR_TIME) - address = service.data.get(ATTR_ADDRESS) - - control_unit = _get_cu_by_interface_id(hass, interface_id) - if control_unit: - await control_unit.central.set_install_mode( - interface_id, t=time, mode=mode, address=address - ) - - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_SET_INSTALL_MODE, - service_func=_service_set_install_mode, - schema=SCHEMA_SERVICE_SET_INSTALL_MODE, - ) - - async def _service_put_paramset(service): - """Service to call the putParamset method on a HomeMatic connection.""" - interface_id = service.data[ATTR_INTERFACE_ID] - address = service.data[ATTR_ADDRESS] - paramset_key = service.data[ATTR_PARAMSET_KEY] - # When passing in the paramset from a YAML file we get an OrderedDict - # here instead of a dict, so add this explicit cast. - # The service schema makes sure that this cast works. - paramset = dict(service.data[ATTR_PARAMSET]) - rx_mode = service.data.get(ATTR_RX_MODE) - - _LOGGER.debug( - "Calling putParamset: %s, %s, %s, %s, %s", - interface_id, - address, - paramset_key, - paramset, - rx_mode, - ) - control_unit = _get_cu_by_interface_id(hass, interface_id) - if control_unit: - await control_unit.central.put_paramset( - interface_id, address, paramset_key, paramset, rx_mode - ) - - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_PUT_PARAMSET, - service_func=_service_put_paramset, - schema=SCHEMA_SERVICE_PUT_PARAMSET, - ) - - -def _get_hm_entity(hass, interface_id, address, parameter) -> GenericEntity: - """Get homematic entity.""" - control_unit = _get_cu_by_interface_id(hass, interface_id) - return control_unit.central.get_hm_entity_by_parameter(address, parameter) - - -def _get_cu_by_interface_id(hass, interface_id) -> ControlUnit | None: - """ - Get ControlUnit by device address - """ - for control_unit in hass.data[DOMAIN].values(): - if control_unit.central.clients.get(interface_id): - return control_unit - return None - - -def _get_hub_by_entity_id(hass, entity_id) -> HmHub | None: - """ - Get ControlUnit by device address - """ - for control_unit in hass.data[DOMAIN].values(): - if control_unit.hub.entity_id == entity_id: - return control_unit.hub - return None diff --git a/custom_components/hahm/binary_sensor.py b/custom_components/hahm/binary_sensor.py index 52f380e7..846d4bec 100644 --- a/custom_components/hahm/binary_sensor.py +++ b/custom_components/hahm/binary_sensor.py @@ -3,22 +3,29 @@ import logging -from hahomematic.const import HA_PLATFORM_BINARY_SENSOR +from hahomematic.const import HmPlatform +from hahomematic.platforms.binary_sensor import HmBinarySensor from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM binary_sensor platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_binary_sensor(args): @@ -31,24 +38,26 @@ def async_add_binary_sensor(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, control_unit.async_signal_new_hm_entity( - entry.entry_id, HA_PLATFORM_BINARY_SENSOR + config_entry.entry_id, HmPlatform.BINARY_SENSOR ), async_add_binary_sensor, ) ) async_add_binary_sensor( - [control_unit.get_hm_entities_by_platform(HA_PLATFORM_BINARY_SENSOR)] + [control_unit.get_hm_entities_by_platform(HmPlatform.BINARY_SENSOR)] ) class HaHomematicBinarySensor(HaHomematicGenericEntity, BinarySensorEntity): """Representation of the Homematic binary sensor.""" + _hm_entity: HmBinarySensor + @property def is_on(self) -> bool: """Return true if motion is detected.""" diff --git a/custom_components/hahm/button.py b/custom_components/hahm/button.py index c99471d6..03ffd09e 100644 --- a/custom_components/hahm/button.py +++ b/custom_components/hahm/button.py @@ -3,22 +3,29 @@ import logging -from hahomematic.const import HA_PLATFORM_BUTTON +from hahomematic.const import HmPlatform +from hahomematic.platforms.button import HmButton from homeassistant.components.button import ButtonEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM binary_sensor platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_button(args): @@ -31,19 +38,23 @@ def async_add_button(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, HA_PLATFORM_BUTTON), + control_unit.async_signal_new_hm_entity( + config_entry.entry_id, HmPlatform.BUTTON + ), async_add_button, ) ) - async_add_button([control_unit.get_hm_entities_by_platform(HA_PLATFORM_BUTTON)]) + async_add_button([control_unit.get_hm_entities_by_platform(HmPlatform.BUTTON)]) class HaHomematicButton(HaHomematicGenericEntity, ButtonEntity): """Representation of the Homematic button.""" + _hm_entity: HmButton + async def async_press(self) -> None: await self._hm_entity.press() diff --git a/custom_components/hahm/climate.py b/custom_components/hahm/climate.py index 25e0a4e4..cfe182d7 100644 --- a/custom_components/hahm/climate.py +++ b/custom_components/hahm/climate.py @@ -4,22 +4,29 @@ import logging from typing import Any -from hahomematic.const import HA_PLATFORM_CLIMATE +from hahomematic.const import HmPlatform +from hahomematic.devices.climate import IPThermostat, RfThermostat, SimpleRfThermostat from homeassistant.components.climate import ClimateEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM climate platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_climate(args): @@ -32,22 +39,24 @@ def async_add_climate(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, control_unit.async_signal_new_hm_entity( - entry.entry_id, HA_PLATFORM_CLIMATE + config_entry.entry_id, HmPlatform.CLIMATE ), async_add_climate, ) ) - async_add_climate([control_unit.get_hm_entities_by_platform(HA_PLATFORM_CLIMATE)]) + async_add_climate([control_unit.get_hm_entities_by_platform(HmPlatform.CLIMATE)]) class HaHomematicClimate(HaHomematicGenericEntity, ClimateEntity): """Representation of the HomematicIP climate entity.""" + _hm_entity: SimpleRfThermostat | RfThermostat | IPThermostat + @property def temperature_unit(self) -> str: """Return the unit of measurement.""" diff --git a/custom_components/hahm/config_flow.py b/custom_components/hahm/config_flow.py index bfb1c8d9..6091363b 100644 --- a/custom_components/hahm/config_flow.py +++ b/custom_components/hahm/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from types import MappingProxyType from typing import Any from xmlrpc.client import ProtocolError @@ -36,11 +37,11 @@ ATTR_INTERFACE_NAME, ATTR_JSON_TLS, ATTR_PATH, - CONF_ENABLE_SENSORS_FOR_OWN_SYSTEM_VARIABLES, + CONF_ENABLE_SENSORS_FOR_SYSTEM_VARIABLES, CONF_ENABLE_VIRTUAL_CHANNELS, DOMAIN, ) -from .controlunit import ControlUnit +from .control_unit import ControlConfig _LOGGER = logging.getLogger(__name__) @@ -70,7 +71,7 @@ async def validate_input( - hass: HomeAssistant, data: dict[str, Any], interface_name: str + hass: HomeAssistant, data: MappingProxyType[str, Any], interface_name: str ) -> bool: """ Validate the user input allows us to connect. @@ -81,7 +82,9 @@ async def validate_input( # it while initializing. config.CACHE_DIR = "cache" - control_unit = ControlUnit(hass, data=data) + control_unit = ControlConfig( + hass=hass, entry_id="validate", data=data + ).get_control_unit() control_unit.create_central() try: await control_unit.create_clients() @@ -153,9 +156,7 @@ async def async_step_interface( errors = {} try: - info = await validate_input( - self.hass, self.data, user_input[ATTR_INTERFACE_NAME] - ) + await validate_input(self.hass, self.data, user_input[ATTR_INTERFACE_NAME]) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -213,8 +214,8 @@ async def async_step_hahm_devices(self, user_input=None): default=self._cu.enable_virtual_channels, ): bool, vol.Optional( - CONF_ENABLE_SENSORS_FOR_OWN_SYSTEM_VARIABLES, - default=self._cu.enable_sensors_for_own_system_variables, + CONF_ENABLE_SENSORS_FOR_SYSTEM_VARIABLES, + default=self._cu.enable_sensors_for_system_variables, ): bool, } ), diff --git a/custom_components/hahm/const.py b/custom_components/hahm/const.py index c35bc930..e60bf29e 100644 --- a/custom_components/hahm/const.py +++ b/custom_components/hahm/const.py @@ -1,4 +1,7 @@ """Constants.""" +from hahomematic.const import AVAILABLE_HM_PLATFORMS + +from homeassistant.const import Platform DOMAIN = "hahm" @@ -16,7 +19,7 @@ ATTR_RX_MODE = "rx_mode" ATTR_VALUE_TYPE = "value_type" -CONF_ENABLE_SENSORS_FOR_OWN_SYSTEM_VARIABLES = "enable_sensors_for_own_system_variables" +CONF_ENABLE_SENSORS_FOR_SYSTEM_VARIABLES = "enable_sensors_for_system_variables" CONF_ENABLE_VIRTUAL_CHANNELS = "enable_virtual_channels" SERVICE_PUT_PARAMSET = "put_paramset" @@ -24,3 +27,18 @@ SERVICE_SET_INSTALL_MODE = "set_install_mode" SERVICE_SET_VARIABLE_VALUE = "set_variable_value" SERVICE_VIRTUAL_KEY = "virtual_key" + + +def _get_hahm_platforms(): + """Return relevant hahm platforms.""" + platforms = [entry.value for entry in Platform] + hm_platforms = [entry.value for entry in AVAILABLE_HM_PLATFORMS] + hahm_platforms = [] + for hm_platform in hm_platforms: + if hm_platform in platforms: + hahm_platforms.append(hm_platform) + + return hahm_platforms + + +HAHM_PLATFORMS = _get_hahm_platforms() diff --git a/custom_components/hahm/controlunit.py b/custom_components/hahm/control_unit.py similarity index 76% rename from custom_components/hahm/controlunit.py rename to custom_components/hahm/control_unit.py index e93c6577..f0ee6478 100644 --- a/custom_components/hahm/controlunit.py +++ b/custom_components/hahm/control_unit.py @@ -8,6 +8,8 @@ from datetime import timedelta import logging +from types import MappingProxyType +from typing import Any from hahomematic import config from hahomematic.central_unit import CentralConfig, CentralUnit @@ -23,7 +25,7 @@ ATTR_TLS, ATTR_USERNAME, ATTR_VERIFY_TLS, - HA_PLATFORMS, + AVAILABLE_HM_PLATFORMS, HH_EVENT_DELETE_DEVICES, HH_EVENT_DEVICES_CREATED, HH_EVENT_ERROR, @@ -34,12 +36,13 @@ HH_EVENT_UPDATE_DEVICE, IP_ANY_V4, PORT_ANY, + HmEventType, + HmPlatform, ) from hahomematic.entity import BaseEntity from hahomematic.hub import HmHub from hahomematic.xml_rpc_server import register_xml_rpc_server -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client, device_registry as dr @@ -53,9 +56,8 @@ ATTR_INTERFACE, ATTR_JSON_TLS, ATTR_PATH, - CONF_ENABLE_SENSORS_FOR_OWN_SYSTEM_VARIABLES, - CONF_ENABLE_VIRTUAL_CHANNELS, DOMAIN, + HAHM_PLATFORMS, ) _LOGGER = logging.getLogger(__name__) @@ -67,30 +69,19 @@ class ControlUnit: Central point to control a Homematic CCU. """ - def __init__(self, hass: HomeAssistant, data=None, entry: ConfigEntry = None): - if data is None: - data = {} - self._hass = hass - self._data = data - if entry: - self._entry = entry - self._entry_id = entry.entry_id - self._data = self._entry.data - self.enable_virtual_channels = self._entry.options.get( - CONF_ENABLE_VIRTUAL_CHANNELS, False - ) - self.enable_sensors_for_own_system_variables = self._entry.options.get( - CONF_ENABLE_SENSORS_FOR_OWN_SYSTEM_VARIABLES, False - ) - else: - self._entry_id = "solo" - self.enable_virtual_channels = False - self.enable_sensors_for_own_system_variables = False + def __init__(self, control_config): + self._hass = control_config.hass + self._entry_id = control_config.entry_id + self._data = control_config.data + self.enable_virtual_channels = control_config.enable_virtual_channels + self.enable_sensors_for_system_variables = ( + control_config.enable_sensors_for_system_variables + ) self._central: CentralUnit = None self._active_hm_entities: dict[str, BaseEntity] = {} self._hub = None - async def start(self): + async def start(self) -> None: """Start the control unit.""" _LOGGER.debug("Starting HAHM ControlUnit %s", self._data[ATTR_INSTANCE_NAME]) config.CACHE_DIR = "cache" @@ -102,7 +93,7 @@ async def start(self): await self.init_clients() self._central.start_connection_checker() - async def stop(self): + async def stop(self) -> None: """Stop the control unit.""" _LOGGER.debug("Stopping HAHM ControlUnit %s", self._data[ATTR_INSTANCE_NAME]) await self._central.stop_connection_checker() @@ -110,7 +101,7 @@ async def stop(self): await client.proxy_de_init() await self._central.stop() - async def init_hub(self): + async def init_hub(self) -> None: """Init the hub.""" await self._central.init_hub() self._hub = HaHub(self._hass, self) @@ -125,7 +116,7 @@ async def init_hub(self): ) @property - def hub(self): + def hub(self) -> HaHub: """Return the Hub.""" return self._hub @@ -135,30 +126,30 @@ async def init_clients(self): await client.proxy_init() @property - def central(self): + def central(self) -> CentralUnit: """return the HAHM central_unit instance.""" return self._central - def get_new_hm_entities(self, new_entities): + def get_new_hm_entities(self, new_entities) -> dict[HmPlatform, list[BaseEntity]]: """ Return all hm-entities by requested unique_ids """ # init dict - hm_entities = {} - for platform in HA_PLATFORMS: - hm_entities[platform] = [] + hm_entities: dict[HmPlatform, list[BaseEntity]] = {} + for hm_platform in AVAILABLE_HM_PLATFORMS: + hm_entities[hm_platform] = [] for entity in new_entities: if ( entity.unique_id not in self._active_hm_entities and entity.create_in_ha - and entity.platform in HA_PLATFORMS + and entity.platform.value in HAHM_PLATFORMS ): hm_entities[entity.platform].append(entity) return hm_entities - def get_hm_entities_by_platform(self, platform): + def get_hm_entities_by_platform(self, platform: HmPlatform) -> list[BaseEntity]: """ Return all hm-entities by platform """ @@ -173,22 +164,22 @@ def get_hm_entities_by_platform(self, platform): return hm_entities - def add_hm_entity(self, hm_entity): + def add_hm_entity(self, hm_entity) -> None: """add entity to active entities""" self._active_hm_entities[hm_entity.unique_id] = hm_entity - def remove_hm_entity(self, hm_entity): + def remove_hm_entity(self, hm_entity) -> None: """remove entity from active entities""" del self._active_hm_entities[hm_entity.unique_id] # pylint: disable=no-self-use @callback - def async_signal_new_hm_entity(self, entry_id, device_type) -> str: + def async_signal_new_hm_entity(self, entry_id: str, device_type) -> str: """Gateway specific event to signal new device.""" return f"hahm-new-entity-{entry_id}-{device_type}" @callback - def _callback_system_event(self, src, *args): + def _callback_system_event(self, src: str, *args): """Callback for ccu based events.""" if src == HH_EVENT_DEVICES_CREATED: new_entity_unique_ids = args[1] @@ -196,13 +187,13 @@ def _callback_system_event(self, src, *args): for (platform, hm_entities) in self.get_new_hm_entities( new_entity_unique_ids ).items(): - args = [] if hm_entities and len(hm_entities) > 0: - args.append([hm_entities]) async_dispatcher_send( self._hass, self.async_signal_new_hm_entity(self._entry_id, platform), - *args, # Don't send device if None, it would override default value in listeners + [ + hm_entities + ], # Don't send device if None, it would override default value in listeners ) elif src == HH_EVENT_NEW_DEVICES: # ignore @@ -210,8 +201,7 @@ def _callback_system_event(self, src, *args): elif src == HH_EVENT_DELETE_DEVICES: # Handle event of device removed in HAHM. for address in args[1]: - entity = self._get_active_entity_by_address(address) - if entity: + if entity := self._get_active_entity_by_address(address): entity.remove_entity() return elif src == HH_EVENT_ERROR: @@ -226,30 +216,32 @@ def _callback_system_event(self, src, *args): return @callback - def _callback_click_event(self, event_type, event_data): + def _callback_click_event( + self, hm_event_type: HmEventType, event_data: dict[str, Any] + ): """Fire event on click.""" - device_id = self._get_device_id(event_data[ATTR_ADDRESS]) - if device_id: + if device_id := self._get_device_id(event_data[ATTR_ADDRESS]): event_data[CONF_DEVICE_ID] = device_id self._hass.bus.fire( - event_type, + hm_event_type.value, event_data, ) @callback - def _callback_alarm_event(self, event_type, event_data): + def _callback_alarm_event( + self, hm_event_type: HmEventType, event_data: dict[str, Any] + ): """Fire event on alarm.""" - device_id = self._get_device_id(event_data[ATTR_ADDRESS]) - if device_id: + if device_id := self._get_device_id(event_data[ATTR_ADDRESS]): event_data[CONF_DEVICE_ID] = device_id self._hass.bus.fire( - event_type, + hm_event_type.value, event_data, ) - def _get_device_id(self, address): + def _get_device_id(self, address: str) -> str | None: """Return the device id of the hahm device.""" hm_device = self.central.hm_devices.get(address) identifiers = hm_device.device_info.get("identifiers") @@ -257,7 +249,7 @@ def _get_device_id(self, address): device = device_registry.async_get_device(identifiers) return device.id if device else None - def create_central(self): + def create_central(self) -> None: """create the central unit for ccu callbacks.""" xml_rpc_server = register_xml_rpc_server( local_ip=self._data.get(ATTR_CALLBACK_HOST), @@ -278,14 +270,14 @@ def create_central(self): json_port=self._data[ATTR_JSON_PORT], json_tls=self._data[ATTR_JSON_TLS], enable_virtual_channels=self.enable_virtual_channels, - enable_sensors_for_own_system_variables=self.enable_sensors_for_own_system_variables, + enable_sensors_for_system_variables=self.enable_sensors_for_system_variables, ).get_central() # register callback self._central.callback_system_event = self._callback_system_event self._central.callback_click_event = self._callback_click_event self._central.callback_alarm_event = self._callback_alarm_event - async def create_clients(self): + async def create_clients(self) -> set[Client]: """create clients for the central unit.""" clients: set[Client] = set() for interface_name in self._data[ATTR_INTERFACE]: @@ -306,12 +298,34 @@ async def create_clients(self): ) return clients - def _get_active_entity_by_address(self, address): + def _get_active_entity_by_address(self, address: str) -> BaseEntity: for entity in self._active_hm_entities.values(): if entity.address == address: return entity +class ControlConfig: + """Config for a ControlUnit.""" + + def __init__( + self, + hass: HomeAssistant, + entry_id: str, + data: MappingProxyType[str, Any], + enable_virtual_channels: bool = False, + enable_sensors_for_system_variables: bool = False, + ) -> None: + self.hass = hass + self.entry_id = entry_id + self.data = data + self.enable_virtual_channels = enable_virtual_channels + self.enable_sensors_for_system_variables = enable_sensors_for_system_variables + + def get_control_unit(self) -> ControlUnit: + """Identify the used client.""" + return ControlUnit(self) + + class HaHub(Entity): """The HomeMatic hub. (CCU2/HomeGear).""" @@ -324,7 +338,7 @@ def __init__(self, hass, cu: ControlUnit): self.entity_id = f"{DOMAIN}.{slugify(self._name.lower())}" self._hm_hub.register_update_callback(self._update_hub) - async def init(self): + async def init(self) -> None: """Init fetch scheduler.""" self.hass.helpers.event.async_track_time_interval( self._fetch_data, SCAN_INTERVAL @@ -337,16 +351,16 @@ def available(self) -> bool: return self._hm_hub.available @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """Return false. HomeMatic Hub object updates variables.""" return False - async def _fetch_data(self, now): + async def _fetch_data(self, now) -> None: """Fetch data from backend.""" await self._hm_hub.fetch_data() @@ -356,16 +370,16 @@ def state(self): return self._hm_hub.state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self._hm_hub.extra_state_attributes @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend, if any.""" return "mdi:gradient-vertical" - async def set_variable(self, name, value): + async def set_variable(self, name: str, value) -> None: """Set variable value on CCU/Homegear.""" sensor = self._hm_hub.hub_entities.get(name) if not sensor or name in self.extra_state_attributes: @@ -382,6 +396,6 @@ async def set_variable(self, name, value): await self._hm_hub.set_system_variable(name, value) @callback - def _update_hub(self, *args): + def _update_hub(self, *args) -> None: """Update the HA hub.""" self.async_schedule_update_ha_state(True) diff --git a/custom_components/hahm/cover.py b/custom_components/hahm/cover.py index 89bf9821..500112bb 100644 --- a/custom_components/hahm/cover.py +++ b/custom_components/hahm/cover.py @@ -4,27 +4,33 @@ from abc import ABC import logging -from hahomematic.const import HA_PLATFORM_COVER -from hahomematic.devices.cover import HmBlind, HmCover +from hahomematic.const import HmPlatform +from hahomematic.devices.cover import HmBlind, HmCover, HmGarage from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, CoverEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM cover platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_cover(args): @@ -34,26 +40,30 @@ def async_add_cover(args): for hm_entity in args[0]: if isinstance(hm_entity, HmBlind): entities.append(HaHomematicBlind(control_unit, hm_entity)) - elif isinstance(hm_entity, HmCover): + elif isinstance(hm_entity, (HmCover, HmGarage)): entities.append(HaHomematicCover(control_unit, hm_entity)) if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, HA_PLATFORM_COVER), + control_unit.async_signal_new_hm_entity( + config_entry.entry_id, HmPlatform.COVER + ), async_add_cover, ) ) - async_add_cover([control_unit.get_hm_entities_by_platform(HA_PLATFORM_COVER)]) + async_add_cover([control_unit.get_hm_entities_by_platform(HmPlatform.COVER)]) class HaHomematicCover(HaHomematicGenericEntity, CoverEntity): """Representation of the HomematicIP cover entity.""" + _hm_entity: HmCover | HmGarage + @property def current_cover_position(self) -> int | None: """ @@ -89,6 +99,8 @@ async def async_stop_cover(self, **kwargs) -> None: class HaHomematicBlind(HaHomematicCover, CoverEntity, ABC): """Representation of the HomematicIP blind entity.""" + _hm_entity: HmBlind + @property def current_cover_tilt_position(self) -> int | None: """ @@ -113,3 +125,9 @@ async def async_close_cover_tilt(self, **kwargs) -> None: async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" await self._hm_entity.stop_cover_tilt() + + +class HaHomematicGarage(HaHomematicCover, CoverEntity): + """Representation of the HomematicIP garage entity.""" + + _hm_entity: HmGarage diff --git a/custom_components/hahm/device_trigger.py b/custom_components/hahm/device_trigger.py index 63a84260..f4a568e0 100644 --- a/custom_components/hahm/device_trigger.py +++ b/custom_components/hahm/device_trigger.py @@ -25,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit CONF_INTERFACE_ID = "interface_id" CONF_EVENT_TYPE = "event_type" @@ -44,18 +44,18 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, Any]] | None: """List device triggers for Home Assistant Homematic(IP) devices.""" device_registry = dr.async_get(hass) - device = device_registry.async_get(device_id) + if (device := device_registry.async_get(device_id)) is None: + return None address = list(device.identifiers)[0][1] if address.endswith(tuple(HM_VIRTUAL_REMOTES)): address = address.split("_")[1] triggers = [] for entry_id in device.config_entries: control_unit: ControlUnit = hass.data[DOMAIN][entry_id] - hm_device = control_unit.central.hm_devices.get(address) - if hm_device: + if hm_device := control_unit.central.hm_devices.get(address): for action_event in hm_device.action_events.values(): if isinstance(action_event, ImpulseEvent): continue diff --git a/custom_components/hahm/generic_entity.py b/custom_components/hahm/generic_entity.py index 1c120f52..f68a04ae 100644 --- a/custom_components/hahm/generic_entity.py +++ b/custom_components/hahm/generic_entity.py @@ -10,8 +10,9 @@ from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_registry import EntityRegistry -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .helper import get_entity_description _LOGGER = logging.getLogger(__name__) @@ -28,8 +29,7 @@ def __init__( """Initialize the generic entity.""" self._cu = control_unit self._hm_entity = hm_entity - entity_description = get_entity_description(self._hm_entity) - if entity_description: + if entity_description := get_entity_description(self._hm_entity): self.entity_description = entity_description # Marker showing that the Hm device hase been removed. self.hm_device_removed = False @@ -89,14 +89,11 @@ async def _init_data(self) -> None: load_state = await self._hm_entity.load_data() # if load_state == DATA_LOAD_FAIL and not self.registry_entry.disabled_by: # await self._update_registry_entry(disabled_by=er.DISABLED_INTEGRATION) - # elif self.registry_entry.disabled_by == er.DISABLED_INTEGRATION: - # await self._update_registry_entry(disabled_by=None) async def _update_registry_entry(self, disabled_by) -> None: """Update registry_entry disabled_by.""" - (await er.async_get_registry(self.hass)).async_update_entity( - self.entity_id, disabled_by=disabled_by - ) + entity_registry: EntityRegistry = await er.async_get_registry(self.hass) + entity_registry.async_update_entity(self.entity_id, disabled_by=disabled_by) @callback def _async_device_changed(self, *args, **kwargs) -> None: @@ -134,8 +131,7 @@ async def async_remove_from_registries(self) -> None: if not self.registry_entry: return - device_id = self.registry_entry.device_id - if device_id: + if device_id := self.registry_entry.device_id: # Remove from device registry. device_registry = await dr.async_get_registry(self.hass) if device_id in device_registry.devices: @@ -144,8 +140,7 @@ async def async_remove_from_registries(self) -> None: else: # Remove from entity registry. # Only relevant for entities that do not belong to a device. - entity_id = self.registry_entry.entity_id - if entity_id: + if entity_id := self.registry_entry.entity_id: entity_registry = await er.async_get_registry(self.hass) if entity_id in entity_registry.entities: entity_registry.async_remove(entity_id) diff --git a/custom_components/hahm/helper.py b/custom_components/hahm/helper.py index 52594329..61dad097 100644 --- a/custom_components/hahm/helper.py +++ b/custom_components/hahm/helper.py @@ -2,61 +2,31 @@ from __future__ import annotations import logging +from typing import Any -from hahomematic.const import ( - HA_PLATFORM_BINARY_SENSOR, - HA_PLATFORM_BUTTON, - HA_PLATFORM_COVER, - HA_PLATFORM_SENSOR, - HA_PLATFORM_SWITCH, -) -from hahomematic.entity import CustomEntity, GenericEntity +from hahomematic.const import HmPlatform +from hahomematic.entity import BaseEntity, CustomEntity, GenericEntity from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_PRESENCE, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_WINDOW, + BinarySensorDeviceClass, BinarySensorEntityDescription, ) -from homeassistant.components.cover import ( - DEVICE_CLASS_BLIND, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_SHADE, - DEVICE_CLASS_SHUTTER, - CoverEntityDescription, -) +from homeassistant.components.button import ButtonEntityDescription +from homeassistant.components.cover import CoverDeviceClass, CoverEntityDescription from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) -from homeassistant.components.switch import ( - DEVICE_CLASS_OUTLET, - DEVICE_CLASS_SWITCH, - SwitchEntityDescription, -) +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntityDescription from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEGREE, - DEVICE_CLASS_CO2, - DEVICE_CLASS_CURRENT, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_SYSTEM, FREQUENCY_HERTZ, LENGTH_MILLIMETERS, LIGHT_LUX, @@ -93,117 +63,117 @@ "HUMIDITY": SensorEntityDescription( key="HUMIDITY", native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), "ACTUAL_TEMPERATURE": SensorEntityDescription( key="ACTUAL_TEMPERATURE", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "TEMPERATURE": SensorEntityDescription( key="TEMPERATURE", native_unit_of_measurement=TEMP_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "LUX": SensorEntityDescription( key="LUX", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "CURRENT_ILLUMINATION": SensorEntityDescription( key="CURRENT_ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "ILLUMINATION": SensorEntityDescription( key="ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "AVERAGE_ILLUMINATION": SensorEntityDescription( key="AVERAGE_ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "LOWEST_ILLUMINATION": SensorEntityDescription( key="LOWEST_ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "HIGHEST_ILLUMINATION": SensorEntityDescription( key="HIGHEST_ILLUMINATION", native_unit_of_measurement=LIGHT_LUX, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "POWER": SensorEntityDescription( key="POWER", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), "IEC_POWER": SensorEntityDescription( key="IEC_POWER", native_unit_of_measurement=POWER_WATT, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), "CURRENT": SensorEntityDescription( key="CURRENT", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), "CONCENTRATION": SensorEntityDescription( key="CONCENTRATION", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=DEVICE_CLASS_CO2, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, ), "ENERGY_COUNTER": SensorEntityDescription( key="ENERGY_COUNTER", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), "IEC_ENERGY_COUNTER": SensorEntityDescription( key="IEC_ENERGY_COUNTER", native_unit_of_measurement=ENERGY_WATT_HOUR, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), "VOLTAGE": SensorEntityDescription( key="VOLTAGE", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "OPERATING_VOLTAGE": SensorEntityDescription( key="OPERATING_VOLTAGE", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "GAS_POWER": SensorEntityDescription( key="GAS_POWER", native_unit_of_measurement=VOLUME_CUBIC_METERS, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.MEASUREMENT, ), "GAS_ENERGY_COUNTER": SensorEntityDescription( key="GAS_ENERGY_COUNTER", native_unit_of_measurement=VOLUME_CUBIC_METERS, - device_class=DEVICE_CLASS_GAS, - state_class=STATE_CLASS_TOTAL_INCREASING, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, ), "RAIN_COUNTER": SensorEntityDescription( key="RAIN_COUNTER", @@ -229,8 +199,8 @@ "AIR_PRESSURE": SensorEntityDescription( key="AIR_PRESSURE", native_unit_of_measurement=PRESSURE_HPA, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), "FREQUENCY": SensorEntityDescription( key="FREQUENCY", @@ -273,201 +243,209 @@ ), } -_SENSOR_DESCRIPTIONS_BY_DEVICE_PARAM: dict[(str, str), SensorEntityDescription] = { +_SENSOR_DESCRIPTIONS_BY_DEVICE_PARAM: dict[tuple[str, str], SensorEntityDescription] = { ("HmIP-SRH", "STATE"): SensorEntityDescription( key="STATE", - device_class=DEVICE_CLASS_WINDOW, + device_class="hahm__srh", ), } _BINARY_SENSOR_DESCRIPTIONS_BY_PARAM: dict[str, BinarySensorEntityDescription] = { "ALARMSTATE": BinarySensorEntityDescription( key="ALARMSTATE", - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, ), "ACOUSTIC_ALARM_ACTIVE": BinarySensorEntityDescription( key="ACOUSTIC_ALARM_ACTIVE", - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, ), "OPTICAL_ALARM_ACTIVE": BinarySensorEntityDescription( key="OPTICAL_ALARM_ACTIVE", - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, ), "DUTY_CYCLE": BinarySensorEntityDescription( key="DUTY_CYCLE", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "DUTYCYCLE": BinarySensorEntityDescription( key="DUTYCYCLE", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "HEATER_STATE": BinarySensorEntityDescription( key="HEATER_STATE", - device_class=DEVICE_CLASS_HEAT, + device_class=BinarySensorDeviceClass.HEAT, ), "LOW_BAT": BinarySensorEntityDescription( key="LOW_BAT", - device_class=DEVICE_CLASS_BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "LOWBAT": BinarySensorEntityDescription( key="LOWBAT", - device_class=DEVICE_CLASS_BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "MOISTURE_DETECTED": BinarySensorEntityDescription( key="MOISTURE_DETECTED", - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, ), "MOTION": BinarySensorEntityDescription( key="MOTION", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), "PRESENCE_DETECTION_STATE": BinarySensorEntityDescription( key="PRESENCE_DETECTION_STATE", - device_class=DEVICE_CLASS_PRESENCE, + device_class=BinarySensorDeviceClass.PRESENCE, ), "RAINING": BinarySensorEntityDescription( key="RAINING", - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, ), "SABOTAGE": BinarySensorEntityDescription( key="SABOTAGE", - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "WATERLEVEL_DETECTED": BinarySensorEntityDescription( key="WATERLEVEL_DETECTED", - device_class=DEVICE_CLASS_MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, ), "WINDOW_STATE": BinarySensorEntityDescription( key="WINDOW_STATE", - device_class=DEVICE_CLASS_WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, ), } _BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_PARAM: dict[ - (str, str), BinarySensorEntityDescription + tuple[str, str], BinarySensorEntityDescription ] = { ("HmIP-SWDO-I", "STATE"): BinarySensorEntityDescription( key="STATE", - device_class=DEVICE_CLASS_WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, ), ("HmIP-SWDO", "STATE"): BinarySensorEntityDescription( key="STATE", - device_class=DEVICE_CLASS_WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, ), ("HmIP-SCI", "STATE"): BinarySensorEntityDescription( key="STATE", - device_class=DEVICE_CLASS_SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, ), } _COVER_DESCRIPTIONS_BY_DEVICE: dict[str, CoverEntityDescription] = { "HmIP-BROLL": CoverEntityDescription( key="BROLL", - device_class=DEVICE_CLASS_SHUTTER, + device_class=CoverDeviceClass.SHUTTER, ), "HmIP-FROLL": CoverEntityDescription( key="FROLL", - device_class=DEVICE_CLASS_SHUTTER, + device_class=CoverDeviceClass.SHUTTER, ), "HmIP-BBL": CoverEntityDescription( key="BBL", - device_class=DEVICE_CLASS_BLIND, + device_class=CoverDeviceClass.BLIND, ), "HmIP-FBL": CoverEntityDescription( key="FBL", - device_class=DEVICE_CLASS_BLIND, + device_class=CoverDeviceClass.BLIND, ), "HmIP-DRBLI4": CoverEntityDescription( key="DRBLI4", - device_class=DEVICE_CLASS_BLIND, + device_class=CoverDeviceClass.BLIND, ), "HmIPW-DRBL4": CoverEntityDescription( key="W-DRBL4", - device_class=DEVICE_CLASS_BLIND, + device_class=CoverDeviceClass.BLIND, ), "HmIP-HDM1": CoverEntityDescription( key="HDM1", - device_class=DEVICE_CLASS_SHADE, + device_class=CoverDeviceClass.SHADE, ), "HmIP-MOD-HO": CoverEntityDescription( key="MOD-HO", - device_class=DEVICE_CLASS_GARAGE, + device_class=CoverDeviceClass.GARAGE, ), "HmIP-MOD-TM": CoverEntityDescription( key="MOD-TM", - device_class=DEVICE_CLASS_GARAGE, + device_class=CoverDeviceClass.GARAGE, ), } _SWITCH_DESCRIPTIONS_BY_DEVICE: dict[str, SwitchEntityDescription] = { "HMIP-PS": SwitchEntityDescription( key="PS", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), "HMIP-PSM": SwitchEntityDescription( key="PSM", - device_class=DEVICE_CLASS_OUTLET, + device_class=SwitchDeviceClass.OUTLET, ), } _SWITCH_DESCRIPTIONS_BY_PARAM: dict[str, SwitchEntityDescription] = {} -_SWITCH_DESCRIPTIONS_BY_DEVICE_PARAM: dict[(str, str), SwitchEntityDescription] = {} +_SWITCH_DESCRIPTIONS_BY_DEVICE_PARAM: dict[ + tuple[str, str], SwitchEntityDescription +] = {} -_ENTITY_DESCRIPTION_DEVICE = { - HA_PLATFORM_COVER: _COVER_DESCRIPTIONS_BY_DEVICE, - HA_PLATFORM_SWITCH: _SWITCH_DESCRIPTIONS_BY_DEVICE, +_ENTITY_DESCRIPTION_DEVICE: dict[HmPlatform, Any] = { + HmPlatform.COVER: _COVER_DESCRIPTIONS_BY_DEVICE, + HmPlatform.SWITCH: _SWITCH_DESCRIPTIONS_BY_DEVICE, } -_ENTITY_DESCRIPTION_PARAM = { - HA_PLATFORM_BINARY_SENSOR: _BINARY_SENSOR_DESCRIPTIONS_BY_PARAM, - HA_PLATFORM_SENSOR: _SENSOR_DESCRIPTIONS_BY_PARAM, - HA_PLATFORM_SWITCH: _SWITCH_DESCRIPTIONS_BY_PARAM, +_ENTITY_DESCRIPTION_PARAM: dict[HmPlatform, Any] = { + HmPlatform.BINARY_SENSOR: _BINARY_SENSOR_DESCRIPTIONS_BY_PARAM, + HmPlatform.SENSOR: _SENSOR_DESCRIPTIONS_BY_PARAM, + HmPlatform.SWITCH: _SWITCH_DESCRIPTIONS_BY_PARAM, } -_ENTITY_DESCRIPTION_DEVICE_PARAM = { - HA_PLATFORM_BINARY_SENSOR: _BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_PARAM, - HA_PLATFORM_SENSOR: _SENSOR_DESCRIPTIONS_BY_DEVICE_PARAM, - HA_PLATFORM_SWITCH: _SWITCH_DESCRIPTIONS_BY_DEVICE_PARAM, +_ENTITY_DESCRIPTION_DEVICE_PARAM: dict[HmPlatform, Any] = { + HmPlatform.BINARY_SENSOR: _BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_PARAM, + HmPlatform.SENSOR: _SENSOR_DESCRIPTIONS_BY_DEVICE_PARAM, + HmPlatform.SWITCH: _SWITCH_DESCRIPTIONS_BY_DEVICE_PARAM, } -_DEFAULT_DESCRIPTION = { - HA_PLATFORM_BINARY_SENSOR: None, - HA_PLATFORM_COVER: None, - HA_PLATFORM_SENSOR: None, - HA_PLATFORM_SWITCH: SwitchEntityDescription( +_DEFAULT_DESCRIPTION: dict[HmPlatform, Any] = { + HmPlatform.BINARY_SENSOR: None, + HmPlatform.BUTTON: ButtonEntityDescription( + key="button_default", + icon="mdi:gesture-tap", + entity_category=ENTITY_CATEGORY_SYSTEM, + ), + HmPlatform.COVER: None, + HmPlatform.SENSOR: None, + HmPlatform.SWITCH: SwitchEntityDescription( key="switch_default", - device_class=DEVICE_CLASS_SWITCH, + device_class=SwitchDeviceClass.SWITCH, ), } -def get_entity_description(hm_entity) -> EntityDescription | None: +def get_entity_description(hm_entity: BaseEntity) -> EntityDescription | None: """Get the entity_description for platform.""" if isinstance(hm_entity, GenericEntity): - device_description = _ENTITY_DESCRIPTION_DEVICE_PARAM.get( + if device_description := _ENTITY_DESCRIPTION_DEVICE_PARAM.get( hm_entity.platform, {} - ).get((hm_entity.device_type, hm_entity.parameter)) - if device_description: + ).get((hm_entity.device_type, hm_entity.parameter)): return device_description + if hm_entity.parameter in ["STATE"]: return _DEFAULT_DESCRIPTION.get(hm_entity.platform, {}) - param_description = _ENTITY_DESCRIPTION_PARAM.get(hm_entity.platform, {}).get( - hm_entity.parameter - ) - if param_description: + + if param_description := _ENTITY_DESCRIPTION_PARAM.get( + hm_entity.platform, {} + ).get(hm_entity.parameter): return param_description + elif isinstance(hm_entity, CustomEntity): - custom_description = _ENTITY_DESCRIPTION_DEVICE.get(hm_entity.platform, {}).get( - hm_entity.device_type - ) - if custom_description: + if custom_description := _ENTITY_DESCRIPTION_DEVICE.get( + hm_entity.platform, {} + ).get(hm_entity.device_type): return custom_description + if hasattr(hm_entity, "platform"): return _DEFAULT_DESCRIPTION.get(hm_entity.platform, None) return None diff --git a/custom_components/hahm/light.py b/custom_components/hahm/light.py index d2357522..fe1906d3 100644 --- a/custom_components/hahm/light.py +++ b/custom_components/hahm/light.py @@ -4,22 +4,29 @@ import logging from typing import Any -from hahomematic.const import HA_PLATFORM_LIGHT +from hahomematic.const import HmPlatform +from hahomematic.devices.light import HmDimmer, HmLight, IPLightBSL from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR, LightEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM light platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_light(args): @@ -32,20 +39,24 @@ def async_add_light(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, HA_PLATFORM_LIGHT), + control_unit.async_signal_new_hm_entity( + config_entry.entry_id, HmPlatform.LIGHT + ), async_add_light, ) ) - async_add_light([control_unit.get_hm_entities_by_platform(HA_PLATFORM_LIGHT)]) + async_add_light([control_unit.get_hm_entities_by_platform(HmPlatform.LIGHT)]) class HaHomematicLight(HaHomematicGenericEntity, LightEntity): """Representation of the HomematicIP light entity.""" + _hm_entity: HmDimmer | HmLight | IPLightBSL + @property def is_on(self) -> bool: """Return true if dimmer is on.""" diff --git a/custom_components/hahm/lock.py b/custom_components/hahm/lock.py index de440474..9bccbbe5 100644 --- a/custom_components/hahm/lock.py +++ b/custom_components/hahm/lock.py @@ -4,22 +4,29 @@ import logging from typing import Any -from hahomematic.const import HA_PLATFORM_LOCK +from hahomematic.const import HmPlatform +from hahomematic.devices.lock import IpLock, RfLock from homeassistant.components.lock import SUPPORT_OPEN, LockEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM lock platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_lock(args): @@ -32,20 +39,24 @@ def async_add_lock(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, HA_PLATFORM_LOCK), + control_unit.async_signal_new_hm_entity( + config_entry.entry_id, HmPlatform.LOCK + ), async_add_lock, ) ) - async_add_lock([control_unit.get_hm_entities_by_platform(HA_PLATFORM_LOCK)]) + async_add_lock([control_unit.get_hm_entities_by_platform(HmPlatform.LOCK)]) class HaHomematicLock(HaHomematicGenericEntity, LockEntity): """Representation of the HomematicIP lock entity.""" + _hm_entity: IpLock | RfLock + @property def is_locked(self): """Return true if lock is on.""" diff --git a/custom_components/hahm/manifest.json b/custom_components/hahm/manifest.json index 753a03fb..d3a76c71 100644 --- a/custom_components/hahm/manifest.json +++ b/custom_components/hahm/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://github.com/danielperna84/custom_homematic", "issue_tracker": "https://github.com/danielperna84/custom_homematic/issues", - "requirements": ["hahomematic==0.0.16"], + "requirements": ["hahomematic==0.0.17"], "ssdp": [], "zeroconf": [], "homekit": {}, "dependencies": [], "codeowners": ["@danielperna84", "@SukramJ"], "iot_class": "local_push", - "version": "0.0.16" + "version": "0.0.17" } diff --git a/custom_components/hahm/number.py b/custom_components/hahm/number.py index eb4336ab..b1bc8f78 100644 --- a/custom_components/hahm/number.py +++ b/custom_components/hahm/number.py @@ -3,23 +3,30 @@ import logging -from hahomematic.const import HA_PLATFORM_NUMBER +from hahomematic.const import HmPlatform +from hahomematic.platforms.number import HmNumber from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM number platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_number(args): @@ -32,20 +39,24 @@ def async_add_number(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, HA_PLATFORM_NUMBER), + control_unit.async_signal_new_hm_entity( + config_entry.entry_id, HmPlatform.NUMBER + ), async_add_number, ) ) - async_add_number([control_unit.get_hm_entities_by_platform(HA_PLATFORM_NUMBER)]) + async_add_number([control_unit.get_hm_entities_by_platform(HmPlatform.NUMBER)]) class HaHomematicNumber(HaHomematicGenericEntity, NumberEntity): """Representation of the HomematicIP number entity.""" + _hm_entity: HmNumber + @property def min_value(self) -> float: """Return the minimum value.""" diff --git a/custom_components/hahm/select.py b/custom_components/hahm/select.py index 8a69460f..358cae6f 100644 --- a/custom_components/hahm/select.py +++ b/custom_components/hahm/select.py @@ -3,23 +3,30 @@ import logging -from hahomematic.const import HA_PLATFORM_SELECT +from hahomematic.const import HmPlatform +from hahomematic.platforms.select import HmSelect from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM select platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_select(args): @@ -32,20 +39,24 @@ def async_add_select(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, HA_PLATFORM_SELECT), + control_unit.async_signal_new_hm_entity( + config_entry.entry_id, HmPlatform.SELECT + ), async_add_select, ) ) - async_add_select([control_unit.get_hm_entities_by_platform(HA_PLATFORM_SELECT)]) + async_add_select([control_unit.get_hm_entities_by_platform(HmPlatform.SELECT)]) class HaHomematicSelect(HaHomematicGenericEntity, SelectEntity): """Representation of the HomematicIP select entity.""" + _hm_select: HmSelect + @property def options(self) -> list[str]: """Return the options.""" diff --git a/custom_components/hahm/sensor.py b/custom_components/hahm/sensor.py index ea4e5223..9519ef8f 100644 --- a/custom_components/hahm/sensor.py +++ b/custom_components/hahm/sensor.py @@ -4,14 +4,17 @@ from datetime import timedelta import logging -from hahomematic.const import HA_PLATFORM_SENSOR +from hahomematic.const import HmPlatform +from hahomematic.platforms.sensor import HmSensor from homeassistant.components.sensor import SensorEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) @@ -19,9 +22,13 @@ SCAN_INTERVAL = timedelta(seconds=30) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM sensor platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_sensor(args): @@ -45,27 +52,31 @@ def async_add_hub_sensors(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, HA_PLATFORM_SENSOR), + control_unit.async_signal_new_hm_entity( + config_entry.entry_id, HmPlatform.SENSOR + ), async_add_sensor, ) ) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, "hub"), + control_unit.async_signal_new_hm_entity(config_entry.entry_id, "hub"), async_add_hub_sensors, ) ) - async_add_sensor([control_unit.get_hm_entities_by_platform(HA_PLATFORM_SENSOR)]) + async_add_sensor([control_unit.get_hm_entities_by_platform(HmPlatform.SENSOR)]) class HaHomematicSensor(HaHomematicGenericEntity, SensorEntity): """Representation of the HomematicIP sensor entity.""" + _hm_entity: HmSensor + @property def native_value(self): return self._hm_entity.state diff --git a/custom_components/hahm/services.py b/custom_components/hahm/services.py new file mode 100644 index 00000000..4d8e3544 --- /dev/null +++ b/custom_components/hahm/services.py @@ -0,0 +1,240 @@ +""" hahomematic services """ +from __future__ import annotations + +from datetime import datetime +import logging + +from hahomematic.const import ( + ATTR_ADDRESS, + ATTR_INTERFACE_ID, + ATTR_NAME, + ATTR_PARAMETER, + ATTR_VALUE, +) +from hahomematic.entity import GenericEntity +from hahomematic.hub import HmHub +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, ATTR_TIME +from homeassistant.core import HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_PARAMSET, + ATTR_PARAMSET_KEY, + ATTR_RX_MODE, + ATTR_VALUE_TYPE, + DOMAIN, + SERVICE_PUT_PARAMSET, + SERVICE_SET_DEVICE_VALUE, + SERVICE_SET_INSTALL_MODE, + SERVICE_SET_VARIABLE_VALUE, + SERVICE_VIRTUAL_KEY, +) +from .control_unit import ControlUnit + +_LOGGER = logging.getLogger(__name__) + +SCHEMA_SERVICE_VIRTUALKEY = vol.Schema( + { + vol.Optional(ATTR_INTERFACE_ID): cv.string, + vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_PARAMETER): cv.string, + } +) + +SCHEMA_SERVICE_SET_VARIABLE_VALUE = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.string, + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_VALUE): cv.match_all, + } +) + +SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema( + { + vol.Required(ATTR_INTERFACE_ID): cv.string, + vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_PARAMETER): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_VALUE): cv.match_all, + vol.Optional(ATTR_VALUE_TYPE): vol.In( + ["boolean", "dateTime.iso8601", "double", "int", "string"] + ), + vol.Optional(ATTR_INTERFACE_ID): cv.string, + } +) + +SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema( + { + vol.Required(ATTR_INTERFACE_ID): cv.string, + vol.Optional(ATTR_TIME, default=60): cv.positive_int, + vol.Optional(ATTR_MODE, default=1): vol.All(vol.Coerce(int), vol.In([1, 2])), + vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + } +) + +SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema( + { + vol.Required(ATTR_INTERFACE_ID): cv.string, + vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_PARAMSET_KEY): vol.All(cv.string, vol.Upper), + vol.Required(ATTR_PARAMSET): dict, + vol.Optional(ATTR_RX_MODE): vol.All(cv.string, vol.Upper), + } +) + + +async def async_setup_services(hass: HomeAssistant) -> None: + """Setup servives""" + + async def _service_virtualkey(service: ServiceCall): + """Service to handle virtualkey servicecalls.""" + interface_id = service.data[ATTR_INTERFACE_ID] + address = service.data[ATTR_ADDRESS] + parameter = service.data[ATTR_PARAMETER] + + if control_unit := _get_cu_by_interface_id(hass, interface_id): + await control_unit.central.press_virtual_remote_key(address, parameter) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_VIRTUAL_KEY, + service_func=_service_virtualkey, + schema=SCHEMA_SERVICE_VIRTUALKEY, + ) + + async def _service_set_variable_value(service: ServiceCall): + """Service to call setValue method for HomeMatic system variable.""" + entity_id = service.data[ATTR_ENTITY_ID] + name = service.data[ATTR_NAME] + value = service.data[ATTR_VALUE] + + if hub := _get_hub_by_entity_id(hass, entity_id): + await hub.set_variable(name, value) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_VARIABLE_VALUE, + service_func=_service_set_variable_value, + schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE, + ) + + async def _service_set_device_value(service: ServiceCall): + """Service to call setValue method for HomeMatic devices.""" + interface_id = service.data[ATTR_INTERFACE_ID] + address = service.data[ATTR_ADDRESS] + parameter = service.data[ATTR_PARAMETER] + value = service.data[ATTR_VALUE] + + # Convert value into correct XML-RPC Type. + # https://docs.python.org/3/library/xmlrpc.client.html#xmlrpc.client.ServerProxy + if value_type := service.data.get(ATTR_VALUE_TYPE): + if value_type == "int": + value = int(value) + elif value_type == "double": + value = float(value) + elif value_type == "boolean": + value = bool(value) + elif value_type == "dateTime.iso8601": + value = datetime.strptime(value, "%Y%m%dT%H:%M:%S") + else: + # Default is 'string' + value = str(value) + + # Device not found + if ( + hm_entity := _get_hm_entity(hass, interface_id, address, parameter) + ) is None: + _LOGGER.error("%s not found!", address) + return + + await hm_entity.send_value(value) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_DEVICE_VALUE, + service_func=_service_set_device_value, + schema=SCHEMA_SERVICE_SET_DEVICE_VALUE, + ) + + async def _service_set_install_mode(service: ServiceCall): + """Service to set interface_id into install mode.""" + interface_id = service.data[ATTR_INTERFACE_ID] + mode = service.data.get(ATTR_MODE) + time = service.data.get(ATTR_TIME) + address = service.data.get(ATTR_ADDRESS) + + if control_unit := _get_cu_by_interface_id(hass, interface_id): + await control_unit.central.set_install_mode( + interface_id, t=time, mode=mode, address=address + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_INSTALL_MODE, + service_func=_service_set_install_mode, + schema=SCHEMA_SERVICE_SET_INSTALL_MODE, + ) + + async def _service_put_paramset(service: ServiceCall): + """Service to call the putParamset method on a HomeMatic connection.""" + interface_id = service.data[ATTR_INTERFACE_ID] + address = service.data[ATTR_ADDRESS] + paramset_key = service.data[ATTR_PARAMSET_KEY] + # When passing in the paramset from a YAML file we get an OrderedDict + # here instead of a dict, so add this explicit cast. + # The service schema makes sure that this cast works. + paramset = dict(service.data[ATTR_PARAMSET]) + rx_mode = service.data.get(ATTR_RX_MODE) + + _LOGGER.debug( + "Calling putParamset: %s, %s, %s, %s, %s", + interface_id, + address, + paramset_key, + paramset, + rx_mode, + ) + + if control_unit := _get_cu_by_interface_id(hass, interface_id): + await control_unit.central.put_paramset( + interface_id, address, paramset_key, paramset, rx_mode + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_PUT_PARAMSET, + service_func=_service_put_paramset, + schema=SCHEMA_SERVICE_PUT_PARAMSET, + ) + + +def _get_hm_entity( + hass: HomeAssistant, interface_id: str, address: str, parameter: str +) -> GenericEntity | None: + """Get homematic entity.""" + if control_unit := _get_cu_by_interface_id(hass, interface_id): + return control_unit.central.get_hm_entity_by_parameter(address, parameter) + return None + + +def _get_cu_by_interface_id( + hass: HomeAssistant, interface_id: str +) -> ControlUnit | None: + """ + Get ControlUnit by device address + """ + for control_unit in hass.data[DOMAIN].values(): + if control_unit.central.clients.get(interface_id): + return control_unit + return None + + +def _get_hub_by_entity_id(hass: HomeAssistant, entity_id: str) -> HmHub | None: + """ + Get ControlUnit by device address + """ + for control_unit in hass.data[DOMAIN].values(): + if control_unit.hub.entity_id == entity_id: + return control_unit.hub + return None diff --git a/custom_components/hahm/strings.json b/custom_components/hahm/strings.json index ad808c52..e3641ecb 100644 --- a/custom_components/hahm/strings.json +++ b/custom_components/hahm/strings.json @@ -34,7 +34,7 @@ "hahm_devices": { "data": { "enable_virtual_channels": "Enable virtual channels", - "enable_sensors_for_own_system_variables": "Enable sensors for own system variables" + "enable_sensors_for_system_variables": "Enable sensors for system variables" }, "description": "Configure visibility of hahm device types", "title": "Hahm options" diff --git a/custom_components/hahm/strings.sensor.json b/custom_components/hahm/strings.sensor.json new file mode 100644 index 00000000..9c84afaf --- /dev/null +++ b/custom_components/hahm/strings.sensor.json @@ -0,0 +1,9 @@ +{ + "state": { + "hahm__srh": { + "CLOSED": "Closed", + "TILTED": "Tilted", + "OPEN": "Open" + } + } +} \ No newline at end of file diff --git a/custom_components/hahm/switch.py b/custom_components/hahm/switch.py index b25b787f..8bb06756 100644 --- a/custom_components/hahm/switch.py +++ b/custom_components/hahm/switch.py @@ -3,22 +3,29 @@ import logging -from hahomematic.const import HA_PLATFORM_SWITCH +from hahomematic.const import HmPlatform +from hahomematic.devices.switch import HmSwitch 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.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controlunit import ControlUnit +from .control_unit import ControlUnit from .generic_entity import HaHomematicGenericEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the HAHM switch platform.""" - control_unit: ControlUnit = hass.data[DOMAIN][entry.entry_id] + control_unit: ControlUnit = hass.data[DOMAIN][config_entry.entry_id] @callback def async_add_switch(args): @@ -31,20 +38,24 @@ def async_add_switch(args): if entities: async_add_entities(entities) - entry.async_on_unload( + config_entry.async_on_unload( async_dispatcher_connect( hass, - control_unit.async_signal_new_hm_entity(entry.entry_id, HA_PLATFORM_SWITCH), + control_unit.async_signal_new_hm_entity( + config_entry.entry_id, HmPlatform.SWITCH + ), async_add_switch, ) ) - async_add_switch([control_unit.get_hm_entities_by_platform(HA_PLATFORM_SWITCH)]) + async_add_switch([control_unit.get_hm_entities_by_platform(HmPlatform.SWITCH)]) class HaHomematicSwitch(HaHomematicGenericEntity, SwitchEntity): """Representation of the HomematicIP switch entity.""" + _hm_entity: HmSwitch + @property def is_on(self) -> bool: """Return true if switch is on.""" diff --git a/custom_components/hahm/translations/en.json b/custom_components/hahm/translations/en.json index 6f88e8ab..cdb53d0f 100644 --- a/custom_components/hahm/translations/en.json +++ b/custom_components/hahm/translations/en.json @@ -34,7 +34,7 @@ "hahm_devices": { "data": { "enable_virtual_channels": "Enable virtual channels", - "enable_sensors_for_own_system_variables": "Enable sensors for own system variables" + "enable_sensors_for_system_variables": "Enable sensors for system variables" }, "description": "Configure visibility of hahm device types", "title": "Hahm options" diff --git a/custom_components/hahm/translations/nl.json b/custom_components/hahm/translations/nl.json index df3d61be..d53b6a48 100644 --- a/custom_components/hahm/translations/nl.json +++ b/custom_components/hahm/translations/nl.json @@ -34,7 +34,7 @@ "hahm_devices": { "data": { "enable_virtual_channels": "Virtuele kanalen inschakelen", - "enable_sensors_for_own_system_variables": "Activeer sensoren voor eigen systeemvariabelen" + "enable_sensors_for_system_variables": "Activeer sensoren voor systeemvariabelen" }, "description": "Configureer zichtbaarheid van hahm apparaattypes", "title": "Hahm opties" diff --git a/custom_components/hahm/translations/sensor.de.json b/custom_components/hahm/translations/sensor.de.json new file mode 100644 index 00000000..fd1e6ce6 --- /dev/null +++ b/custom_components/hahm/translations/sensor.de.json @@ -0,0 +1,9 @@ +{ + "state": { + "hahm__srh": { + "CLOSED": "Geschlossen", + "TILTED": "Gekippt", + "OPEN": "Offen" + } + } +} \ No newline at end of file diff --git a/custom_components/hahm/translations/sensor.en.json b/custom_components/hahm/translations/sensor.en.json new file mode 100644 index 00000000..9c84afaf --- /dev/null +++ b/custom_components/hahm/translations/sensor.en.json @@ -0,0 +1,9 @@ +{ + "state": { + "hahm__srh": { + "CLOSED": "Closed", + "TILTED": "Tilted", + "OPEN": "Open" + } + } +} \ No newline at end of file diff --git a/info.md b/info.md new file mode 100644 index 00000000..804673da --- /dev/null +++ b/info.md @@ -0,0 +1,34 @@ +[HA Homematic Custom Component](https://github.com/danielperna84/custom_homematic) for Home Assistant + +This is a custom component to integrate [HA Homematic](https://github.com/danielperna84/hahomematic) into [Home Assistant](https://www.home-assistant.io). + +### This project is still in early development! + +Provides the following: +- basic ConfigFlow +- Optionflow + - Enable virtual channels of HmIP-Devices + - Enable sensors for system variables +- Device Trigger (PRESS_XXX Events are selectable in automations) +- Virtual Remotes can be triggered in HA automations +- The Hub (CCU/Homegear) with all system variables +- Supports TLS to CCU/Homegear for Json and XMLRPC + +Services: +- Put paramset (Call to putParamset in the RPC XML interface) +- Set device value (Set the value of a node) +- Set install mode (Enable the install mode +- Set variable value (Set the value of a system variable) +- Virtual key (Press a virtual key from CCU/Homegear or simulate keypress) + +Entity Support +- Binary_Sensor +- Button +- Climate +- Cover +- Light +- Lock +- Sensor +- Number +- Select +- Switch \ No newline at end of file