From da378658b433f5d9b5f5e50f99364eb32b118dba Mon Sep 17 00:00:00 2001 From: h4de5 Date: Sun, 20 Oct 2024 20:50:01 +0200 Subject: [PATCH] Remove cached_property decorator - fixes #84 --- custom_components/vimar/climate.py | 25 +++--- custom_components/vimar/cover.py | 13 ++- custom_components/vimar/light.py | 23 +++--- custom_components/vimar/manifest.json | 2 +- custom_components/vimar/vimar_entity.py | 36 ++++---- .../vimar/vimarlink/vimarlink.py | 82 +++++++++++++++---- pyrightconfig.json | 14 +++- 7 files changed, 128 insertions(+), 67 deletions(-) diff --git a/custom_components/vimar/climate.py b/custom_components/vimar/climate.py index 10f0200..a538f01 100755 --- a/custom_components/vimar/climate.py +++ b/custom_components/vimar/climate.py @@ -1,6 +1,5 @@ """Platform for climate integration.""" -from functools import cached_property import logging from homeassistant.components.climate import ClimateEntity @@ -113,7 +112,7 @@ def is_on(self): self.get_state("funzionamento") == self.get_const_value(VIMAR_CLIMATE_OFF) ] - @cached_property + @property def supported_features(self): """Flag supported features. The device supports a target temperature.""" flags = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON @@ -123,7 +122,7 @@ def supported_features(self): flags |= ClimateEntityFeature.AUX_HEAT return flags - @cached_property + @property def current_temperature(self): """Return current temperature.""" if self.has_state("temperatura"): @@ -131,23 +130,23 @@ def current_temperature(self): if self.has_state("temperatura_misurata"): return float(self.get_state("temperatura_misurata") or 0) - @cached_property + @property def current_humidity(self): """Return current humidity.""" if self.has_state("umidita"): return float(self.get_state("umidita") or 0) - @cached_property + @property def target_temperature(self): """Return the temperature we try to reach.""" return float(self.get_state("setpoint") or 0) - @cached_property + @property def target_temperature_step(self): """Return the supported step of target temperature.""" return 0.1 - @cached_property + @property def temperature_unit(self): """Return unit of temperature measurement for the system (UnitOfTemperature.CELSIUS or UnitOfTemperature.FAHRENHEIT).""" # TODO - find a way to handle different units from vimar device @@ -158,7 +157,7 @@ def temperature_unit(self): else: return UnitOfTemperature.CELSIUS - @cached_property + @property def hvac_mode(self): """Return target operation (e.g.heat, cool, auto, off). Used to determine state.""" # can be HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF @@ -195,13 +194,13 @@ def hvac_mode(self): # else: # return HVACMode.OFF - @cached_property + @property def hvac_modes(self): """List of available operation modes. See below.""" # button for auto is still there, to clear manual mode, but will not change highlighted icon return [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF, HVACMode.AUTO] - @cached_property + @property def hvac_action(self): """Return current HVAC action (heating, cooling, idle, off).""" # HVACAction.HEATING, HVACAction.COOLING, HVACAction.OFF, HVACAction.IDLE @@ -233,18 +232,18 @@ def hvac_action(self): else: return HVACAction.IDLE - @cached_property + @property def is_aux_heat(self): """Return True if an auxiliary heater is on. Requires ClimateEntityFeature.AUX_HEAT.""" if self.has_state("stato_boost on/off"): return self.get_state("stato_boost on/off") != "0" - @cached_property + @property def fan_modes(self) -> list[str] | None: """Return the list of available fan modes. Requires ClimateEntityFeature.FAN_MODE.""" return [FAN_ON, FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH] - @cached_property + @property def fan_mode(self): """Return the current fan mode. Requires ClimateEntityFeature.FAN_MODE.""" if self.has_state("modalita_fancoil"): diff --git a/custom_components/vimar/cover.py b/custom_components/vimar/cover.py index 585e69e..e2261a5 100755 --- a/custom_components/vimar/cover.py +++ b/custom_components/vimar/cover.py @@ -10,7 +10,6 @@ from .vimar_entity import VimarEntity, vimar_setup_entry from homeassistant.components.cover import CoverEntity from .const import DEVICE_TYPE_COVERS as CURR_PLATFORM -from functools import cached_property _LOGGER = logging.getLogger(__name__) @@ -27,7 +26,7 @@ class VimarCover(VimarEntity, CoverEntity): # see: # https://developers.home-assistant.io/docs/entity_index/#generic-properties # Return True if the state is based on our assumption instead of reading it from the device. this will ignore is_closed state - @cached_property + @property def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" return True @@ -46,7 +45,7 @@ def __init__(self, coordinator, device_id: int): def entity_platform(self): return CURR_PLATFORM - @cached_property + @property def is_closed(self) -> bool | None: """Return if the cover is closed.""" # if _state (stopped) is 1, than stopped was pressed, therefor it cannot be completely closed @@ -62,7 +61,7 @@ def is_closed(self) -> bool | None: else: return None - @cached_property + @property def current_cover_position(self): """Return current position of cover. @@ -73,7 +72,7 @@ def current_cover_position(self): else: return None - @cached_property + @property def current_cover_tilt_position(self): """ Return current position of cover tilt. @@ -85,12 +84,12 @@ def current_cover_tilt_position(self): else: return None - @cached_property + @property def is_default_state(self): """Return True of in default state - resulting in default icon.""" return (self.is_closed, True)[self.is_closed is None] - @cached_property + @property def supported_features(self) -> CoverEntityFeature: """Flag supported features.""" flags = ( diff --git a/custom_components/vimar/light.py b/custom_components/vimar/light.py index 93ceab7..e177674 100755 --- a/custom_components/vimar/light.py +++ b/custom_components/vimar/light.py @@ -1,6 +1,5 @@ """Platform for light integration.""" -from functools import cached_property import logging from typing import Any @@ -38,32 +37,36 @@ def __init__(self, coordinator, device_id: int): def entity_platform(self): return CURR_PLATFORM - @cached_property - def is_on(self): + @property + def is_on(self) -> bool: """Set to True if the device is on.""" return self.get_state("on/off") == "1" - @cached_property + @property def is_default_state(self): """Return True of in default state - resulting in default icon.""" return self.is_on - @cached_property + @property def brightness(self): """Return Brightness of this light between 0..255.""" return self.recalculate_brightness(int(self.get_state("value") or 0)) - @cached_property + @property def rgb_color(self) -> tuple[int, int, int] | None: """Return RGB colors.""" - return (self.get_state("red") or 0, self.get_state("green") or 0, self.get_state("blue") or 0) + return ( + self.get_state("red") or 0, + self.get_state("green") or 0, + self.get_state("blue") or 0, + ) - @cached_property + @property def hs_color(self): """Return the hue and saturation.""" return color_util.color_RGB_to_hs(*self.rgb_color) - @cached_property + @property def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self.has_state("red") and self.has_state("green") and self.has_state("blue"): @@ -72,7 +75,7 @@ def color_mode(self) -> ColorMode: return ColorMode.BRIGHTNESS return ColorMode.ONOFF - @cached_property + @property def supported_color_modes(self) -> set[ColorMode] | None: """Flag supported color modes.""" flags: set[ColorMode] = set() diff --git a/custom_components/vimar/manifest.json b/custom_components/vimar/manifest.json index bf159c2..0898987 100755 --- a/custom_components/vimar/manifest.json +++ b/custom_components/vimar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://github.com/h4de5/home-assistant-vimar", "iot_class": "local_polling", "issue_tracker": "https://github.com/h4de5/home-assistant-vimar/issues", - "version": "2024.10.0" + "version": "2024.10.1" } diff --git a/custom_components/vimar/vimar_entity.py b/custom_components/vimar/vimar_entity.py index be60dc7..d329f94 100755 --- a/custom_components/vimar/vimar_entity.py +++ b/custom_components/vimar/vimar_entity.py @@ -1,7 +1,7 @@ """Insteon base entity.""" -from functools import cached_property import logging +from typing import Dict from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.components.binary_sensor import BinarySensorDeviceClass @@ -18,7 +18,7 @@ _LOGGER, ) from .vimar_coordinator import VimarDataUpdateCoordinator -from .vimarlink.vimarlink import VimarLink, VimarProject +from .vimarlink.vimarlink import VimarDevice, VimarLink, VimarProject class VimarEntity(CoordinatorEntity): @@ -26,7 +26,7 @@ class VimarEntity(CoordinatorEntity): _logger = _LOGGER _logger_is_debug = False - _device = [] + _device: VimarDevice | None = None _device_id = 0 _vimarconnection: VimarLink | None = None _vimarproject: VimarProject | None = None @@ -44,7 +44,7 @@ def __init__(self, coordinator: VimarDataUpdateCoordinator, device_id: int): self._vimarproject = coordinator.vimarproject self._reset_status() - if self._device_id in self._vimarproject.devices: + if self._vimarproject is not None and self._device_id in self._vimarproject.devices: self._device = self._vimarproject.devices[self._device_id] self._logger = logging.getLogger(PACKAGE_NAME + "." + self.entity_platform) self._logger_is_debug = self._logger.isEnabledFor(logging.DEBUG) @@ -61,12 +61,12 @@ def device_name(self): name = self._device["object_name"] return name - @cached_property + @property def name(self): """Return the name of the device.""" return self.device_name - @cached_property + @property def extra_state_attributes(self): """Return device specific state attributes.""" # see: https://developers.home-assistant.io/docs/dev_101_states/ @@ -172,7 +172,7 @@ def has_state(self, state): else: return False - @cached_property + @property def icon(self): """Icon to use in the frontend, if any.""" if isinstance(self._device["icon"], str): @@ -184,12 +184,12 @@ def icon(self): return self.ICON - @cached_property + @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" return self._device["device_class"] - @cached_property + @property def unique_id(self): """Return the ID of this device.""" # self._logger.debug("Unique Id: " + DOMAIN + '_' + self._platform + '_' + self._device_id + " - " + self.name) @@ -201,12 +201,12 @@ def unique_id(self): def _reset_status(self): """Set status from _device to class variables.""" - @cached_property + @property def is_default_state(self): """Return True of in default state - resulting in default icon.""" return False - @cached_property + @property def device_info(self) -> DeviceInfo | None: room_name = None if ( @@ -275,22 +275,22 @@ def __init__(self, coordinator: VimarDataUpdateCoordinator): self._data = self._attributes self._state = False - @cached_property + @property def device_class(self): """Return the class of this sensor.""" return self._type - @cached_property + @property def should_poll(self): """Polling needed for a demo binary sensor.""" return True - @cached_property + @property def name(self): """Return the name of the binary sensor.""" return self._name - @cached_property + @property def unique_id(self): """Return the ID of this device.""" # self._logger.debug("Unique Id: " + DOMAIN + '_' + self._platform + '_' + self._device_id + " - " + self.name) @@ -307,12 +307,12 @@ def device_state_attributes(self): else: return None - @cached_property + @property def extra_state_attributes(self): """Return device specific state attributes.""" return self._attributes - @cached_property + @property def device_info(self): return { "identifiers": { @@ -323,7 +323,7 @@ def device_info(self): "manufacturer": "Vimar", } - @cached_property + @property def is_on(self): """Return true if the binary sensor is on.""" return self._state diff --git a/custom_components/vimar/vimarlink/vimarlink.py b/custom_components/vimar/vimarlink/vimarlink.py index 9fcbf89..fd8c109 100755 --- a/custom_components/vimar/vimarlink/vimarlink.py +++ b/custom_components/vimar/vimarlink/vimarlink.py @@ -1,10 +1,11 @@ """Connection to vimar web server.""" -from functools import cached_property +from collections.abc import Callable import logging import os import ssl import sys +from typing import Dict, List, Tuple, TypedDict from xml.etree import ElementTree import xml.etree.cElementTree as xmlTree @@ -109,6 +110,32 @@ class VimarConnectionError(VimarApiError): pass +# single device +# 'room_ids': number[] (maybe empty, ids of rooms) +# 'object_id': number (unique id of entity) +# 'object_name': str (name of the device, reformated in format_name) +# 'object_type': str (CH_xx channel name of vimar) +# 'status': dict{dict{'status_id': number, 'status_value': str }} +# 'device_type': str (mapped type: light, switch, climate, cover, sensor) +# 'device_class': str (mapped class, based on name or attributes: fan, outlet, window, power) +class VimarDevice(TypedDict): + """Single Vimar device for typing""" + + object_id: str + room_ids: List[int] + room_names: List[str] + room_name: str + object_name: str + object_type: str + status: Dict[str, Dict[str, str]] + + device_type: str + device_class: str + device_friendly_name: str + icon: str + pass + + class VimarLink: """Link to communicate with the Vimar webserver.""" @@ -450,7 +477,15 @@ def get_device_status(self, object_id): # 'IS_WRITABLE' => string '1' (length=1) # 'IS_VISIBLE' => string '1' (length=1) - def get_paged_results(self, method, objectlist={}, start=0): + def get_paged_results( + self, + method: Callable[ + [Dict[str, VimarDevice], int | None, int | None], + Tuple[Dict[str, VimarDevice], int] | None, + ], + objectlist: Dict[str, VimarDevice] = {}, + start=0, + ): """Page results from a method automatically.""" # define a page size limit = MAX_ROWS_PER_REQUEST @@ -466,7 +501,12 @@ def get_paged_results(self, method, objectlist={}, start=0): else: raise VimarApiError("Calling invalid method for paged results: %s", method) - def get_room_devices(self, devices={}, start: int | None = None, limit: int | None = None): + def get_room_devices( + self, + devices: Dict[str, VimarDevice] = {}, + start: int | None = None, + limit: int | None = None, + ): """Load all devices that belong to a room.""" if self._room_ids is None: return None @@ -500,11 +540,18 @@ def get_room_devices(self, devices={}, start: int | None = None, limit: int | No # passo OnlyUpdate a True, poichè deve solo riempire le informazioni delle room per gli oggetti esistenti return self._generate_device_list(select, devices, True) - def get_remote_devices(self, devices={}, start: int | None = None, limit: int | None = None): + def get_remote_devices( + self, + devices: Dict[str, VimarDevice] = {}, + start: int | None = None, + limit: int | None = None, + ): """Get all devices that can be triggered remotly (includes scenes).""" if len(devices) == 0: _LOGGER.debug( - "get_remote_devices started - from %d to %d", start, (start or 0) + (limit or 0) + "get_remote_devices started - from %d to %d", + start, + (start or 0) + (limit or 0), ) start, limit = self._sanitize_limits(start, limit) @@ -533,7 +580,9 @@ def _sanitize_limits(self, start: int | None, limit: int | None): start = 0 return start, limit - def _generate_device_list(self, select, devices={}, onlyUpdate=False): + def _generate_device_list( + self, select, devices: Dict[str, VimarDevice] = {}, onlyUpdate=False + ): """Generate device list from given sql statements.""" payload = self._request_vimar_sql(select) if payload is not None: @@ -544,7 +593,7 @@ def _generate_device_list(self, select, devices={}, onlyUpdate=False): if device["object_id"] not in devices: if onlyUpdate: continue - deviceItem = { + deviceItem: VimarDevice = { "room_ids": [], "room_names": [], "room_name": "", @@ -552,6 +601,10 @@ def _generate_device_list(self, select, devices={}, onlyUpdate=False): "object_name": device["object_name"], "object_type": device["object_type"], "status": {}, + "device_type": "", + "device_class": "", + "device_friendly_name": "", + "icon": "", } devices[device["object_id"]] = deviceItem else: @@ -841,21 +894,12 @@ def _request(self, url, post=None, headers=None, check_ssl=False): class VimarProject: """Container that holds all vimar devices and its states.""" - _devices = {} + _devices: Dict[str, VimarDevice] = {} _link: VimarLink _platforms_exists = {} global_channel_id = None _device_customizer_action = None - # single device - # 'room_ids': number[] (maybe empty, ids of rooms) - # 'object_id': number (unique id of entity) - # 'object_name': str (name of the device, reformated in format_name) - # 'object_type': str (CH_xx channel name of vimar) - # 'status': dict{dict{'status_id': number, 'status_value': str }} - # 'device_type': str (mapped type: light, switch, climate, cover, sensor) - # 'device_class': str (mapped class, based on name or attributes: fan, outlet, window, power) - def __init__(self, link: VimarLink, device_customizer_action=None): """Create new container to hold all states.""" self._link = link @@ -1210,6 +1254,10 @@ def parse_device_type(self, device): def format_name(self, name): """Format device name to get rid of unused terms.""" parts = name.split(" ") + level_name = "" + room_name = "" + device_type = "" + entity_number = "" if len(parts) > 0: if len(parts) >= 4: diff --git a/pyrightconfig.json b/pyrightconfig.json index 18f40f3..b108cbc 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,3 +1,15 @@ { - "reportDeprecated": "error" + "include": [ + "custom_components/vimar", + ], + "exclude": [ + "**/node_modules", + "**/__pycache__", + ], + "pythonVersion": "3.12", + "pythonPlatform": "Linux", + + "reportDeprecated": "error", + "reportIncompatibleMethodOverride": "none", + "reportIncompatibleVariableOverride ": "none", } \ No newline at end of file