diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b37fff11..c6058d23 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,7 +8,7 @@ on: pull_request: env: - DEFAULT_PYTHON: 3.9 + DEFAULT_PYTHON: 3.13 jobs: pre-commit: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5175d42a..f7f5525b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/PyCQA/bandit - rev: '1.7.9' + rev: '1.9.2' hooks: - id: bandit args: @@ -10,7 +10,7 @@ repos: - --configfile=.bandit.yaml files: ^custom_components/hilo/.+\.py$ - repo: https://github.com/python/black - rev: 22.3.0 + rev: 25.12.0 hooks: - id: black args: @@ -19,7 +19,7 @@ repos: language_version: python3 files: ^custom_components/hilo/.+\.py$ - repo: https://github.com/codespell-project/codespell - rev: v1.16.0 + rev: v2.4.1 hooks: - id: codespell args: @@ -29,7 +29,7 @@ repos: - -L ba,hass,que,bord exclude_types: [json] - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: @@ -37,7 +37,7 @@ repos: - pydocstyle==5.0.1 files: ^custom_components/hilo/.+\.py$ - repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 + rev: v5.10.1 hooks: - id: isort additional_dependencies: @@ -52,7 +52,7 @@ repos: # - types-python-dateutil==2.8.0 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v6.0.0 hooks: - id: check-json # Exclude .devcontainer.json and .vscode since it uses JSONC @@ -66,12 +66,12 @@ repos: # - id: pydocstyle # files: ^custom_components/hilo/.+\.py$ - repo: https://github.com/gruntwork-io/pre-commit - rev: v0.1.12 + rev: v0.1.30 hooks: - id: shellcheck files: ^script/.+ - repo: https://github.com/jorisroovers/gitlint - rev: v0.17.0 + rev: v0.19.1 hooks: - id: gitlint name: gitlint @@ -80,7 +80,7 @@ repos: args: [--staged, --msg-filename] stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v6.0.0 hooks: - id: check-yaml - id: trailing-whitespace diff --git a/custom_components/hilo/__init__.py b/custom_components/hilo/__init__.py index 979c7d37..73863b15 100644 --- a/custom_components/hilo/__init__.py +++ b/custom_components/hilo/__init__.py @@ -24,7 +24,7 @@ EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Context, Event, HomeAssistant, callback +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( config_entry_oauth2_flow, @@ -114,8 +114,10 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> def _async_register_custom_device( hass: HomeAssistant, entry: ConfigEntry, device: HiloDevice ) -> None: - """Register a custom device. This is used to register the - Hilo gateway and the unknown source tracker.""" + """Register a custom device. + + This is used to register the Hilo gateway and the unknown source tracker. + """ LOG.debug("Generating custom device %s", device) device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -196,12 +198,12 @@ async def handle_debug_event(event: Event): async def async_reload_entry(_: HomeAssistant, updated_entry: ConfigEntry) -> None: """Handle an options update. + This method will get called in two scenarios: 1. When HiloOptionsFlowHandler is initiated 2. When a new refresh token is saved to the config entry data We only want #1 to trigger an actual reload. """ - nonlocal current_options updated_options = {**updated_entry.options} if updated_options == current_options: return @@ -319,6 +321,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api: API) -> None: self._websocket_listeners = [] def validate_heartbeat(self, event: WebsocketEvent) -> None: + """Validate heartbeat messages from the websocket.""" heartbeat_time = from_utc_timestamp(event.arguments[0]) # type: ignore if self._api.log_traces: LOG.debug("Heartbeat: %s", time_diff(heartbeat_time, event.timestamp)) @@ -519,7 +522,7 @@ async def on_websocket_event(self, event: WebsocketEvent) -> None: @callback async def subscribe_to_location(self, inv_id: int) -> None: - """Sends the json payload to receive updates from the location.""" + """Send the json payload to receive updates from the location.""" LOG.debug("Subscribing to location %s", self.devices.location_id) await self._api.websocket_devices.async_invoke( [self.devices.location_id], "SubscribeToLocation", inv_id @@ -527,7 +530,7 @@ async def subscribe_to_location(self, inv_id: int) -> None: @callback async def subscribe_to_challenge(self, inv_id: int, event_id: int = 0) -> None: - """Sends the json payload to receive updates from the challenge.""" + """Send the json payload to receive updates from the challenge.""" LOG.debug("Subscribing to challenge : %s or %s", event_id, self.challenge_id) event_id = event_id or self.challenge_id LOG.debug("API URN is %s", self._api.urn) @@ -571,7 +574,7 @@ async def subscribe_to_challenge(self, inv_id: int, event_id: int = 0) -> None: @callback async def subscribe_to_challengelist(self, inv_id: int) -> None: - """Sends the json payload to receive updates from the challenge list.""" + """Send the json payload to receive updates from the challenge list.""" # TODO : Rename challegenge functions to Event, fallback on challenge for now LOG.debug( "Subscribing to challenge list at location %s", self.devices.location_id @@ -595,7 +598,7 @@ async def subscribe_to_challengelist(self, inv_id: int) -> None: async def request_challenge_consumption_update( self, inv_id: int, event_id: int = 0 ) -> None: - """Sends the json payload to receive energy consumption updates from the challenge.""" + """Send the json payload to receive energy consumption updates from the challenge.""" event_id = event_id or self.challenge_id # TODO: Remove fallback once split is complete @@ -647,12 +650,14 @@ async def request_challenge_consumption_update( @callback async def request_status_update(self) -> None: + """Request a status update from the device websocket.""" await self._api.websocket_devices.send_status() for inv_id, inv_cb in self.invocations.items(): await inv_cb(inv_id) @callback async def request_status_update_challenge(self) -> None: + """Request a status update from the challenge websocket.""" await self._api.websocket_challenges.send_status() for inv_id, inv_cb in self.invocations.items(): await inv_cb(inv_id) @@ -675,8 +680,8 @@ def _get_unknown_source_tracker(self) -> HiloDevice: } async def get_event_details(self, event_id: int): - """Getting events from Hilo only when necessary. - Otherwise, we hit the cache. + """Get events from Hilo only when necessary, otherwise, we hit the cache. + When preheat is started and our last update is before the preheat_start, we refresh. This should update the allowed_kWh, etc. values. @@ -819,6 +824,7 @@ async def start_websocket_loop(self, websocket, id) -> None: ) async def cancel_task(self, task) -> None: + """Cancel a task.""" LOG.debug("Cancelling task %s", task) if task: task.cancel() @@ -843,7 +849,8 @@ async def cancel_websocket_loop(self, websocket, id) -> None: def should_websocket_reconnect(self) -> bool: """Determine if a websocket should reconnect when the connection is lost. - Currently only used to disable websockets in the unit tests.""" + Currently only used to disable websockets in the unit tests. + """ return self._should_websocket_reconnect @should_websocket_reconnect.setter @@ -852,7 +859,7 @@ def should_websocket_reconnect(self, value: bool) -> None: self._should_websocket_reconnect = value async def async_update(self) -> None: - """Updates tarif periodically.""" + """Update tarif periodically.""" if self.generate_energy_meters or self.track_unknown_sources: self.check_tarif() @@ -860,6 +867,7 @@ async def async_update(self) -> None: self.handle_unknown_power() def find_meter(self, hass): + """Find the smart meter entity in Home Assistant.""" entity_registry_dict = {} registry = hass.data.get("entity_registry") @@ -892,6 +900,7 @@ def find_meter(self, hass): return ", ".join(filtered_names) if filtered_names else "" def set_state(self, entity, state, new_attrs={}, keep_state=False, force=False): + """Set the state of an entity.""" params = f"{entity=} {state=} {new_attrs=} {keep_state=}" current = self._hass.states.get(entity) if not current: @@ -913,6 +922,7 @@ def set_state(self, entity, state, new_attrs={}, keep_state=False, force=False): @property def high_times(self): + """Check if the current time is within high tariff periods.""" challenge_sensor = self._hass.states.get("sensor.defi_hilo") LOG.debug( "high_times check tarif challenge sensor is %s", challenge_sensor.state @@ -920,13 +930,13 @@ def high_times(self): return challenge_sensor.state == "reduction" def check_season(self): - """This logic determines if we are using a winter or summer rate""" + """Determine if we are using a winter or summer rate.""" current_month = datetime.now().month LOG.debug("check_season current month is %s", current_month) return current_month in [12, 1, 2, 3] def check_tarif(self): - """Logic to determine which tarif to select depending on season and user-selected rate""" + """Determine which tarif to select depending on season and user-selected rate.""" if self.generate_energy_meters: season = self.check_season() LOG.debug("check_tarif current season state is %s", season) @@ -989,7 +999,7 @@ def check_tarif(self): self.set_tarif(entity, state.state, tarif) def handle_unknown_power(self): - """Function that takes care of the unknown source meter""" + """Take care of the unknown source meter.""" known_power = 0 smart_meter = self.find_meter(self._hass) LOG.debug("Smart meter used currently is: %s", smart_meter) @@ -1044,7 +1054,7 @@ def handle_unknown_power(self): @callback def fix_utility_sensor(self, entity, state): - """not sure why this doesn't get created with a proper device_class""" + """Not sure why this doesn't get created with a proper device_class.""" current_state = state.as_dict() attrs = current_state.get("attributes", {}) if entity.startswith("select.") or entity.find("hilo_rate") > 0: @@ -1075,6 +1085,7 @@ def fix_utility_sensor(self, entity, state): @callback def set_tarif(self, entity, current, new): + """Set the tarif on the select entity if needed.""" if self.untarificated_devices and entity != f"select.{HILO_ENERGY_TOTAL}": return if entity.startswith("select.hilo_energy") and current != new: @@ -1149,4 +1160,5 @@ def async_migrate_unique_id( @callback def handle_subscription_result(self, hilo_id: str) -> None: + """Handle subscription result by notifying entities.""" async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(hilo_id)) diff --git a/custom_components/hilo/climate.py b/custom_components/hilo/climate.py index aafb3925..ecbf42b0 100755 --- a/custom_components/hilo/climate.py +++ b/custom_components/hilo/climate.py @@ -1,3 +1,5 @@ +"""Support for Hilo Climate entities.""" + from datetime import datetime, timedelta from homeassistant.components.climate import ClimateEntity @@ -23,6 +25,7 @@ def validate_reduction_phase(events, tag): + """Validate if current time is within a challenge lock reduction phase.""" if not events: return current = events[0] @@ -44,6 +47,7 @@ def validate_reduction_phase(events, tag): async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: + """Set up Hilo climate entities from a config entry.""" hilo = hass.data[DOMAIN][entry.entry_id] entities = [] for d in hilo.devices.all: @@ -55,12 +59,15 @@ async def async_setup_entry( class HiloClimate(HiloEntity, ClimateEntity): + """Representation of a Hilo Climate entity.""" + _attr_hvac_modes = [HVACMode.HEAT] _attr_temperature_unit: str = UnitOfTemperature.CELSIUS _attr_precision: float = PRECISION_TENTHS _attr_supported_features: int = ClimateEntityFeature.TARGET_TEMPERATURE def __init__(self, hilo: Hilo, device): + """Initialize the climate entity.""" super().__init__(hilo, device=device, name=device.name) old_unique_id = f"{slugify(device.name)}-climate" self._attr_unique_id = f"{slugify(device.identifier)}-climate" @@ -74,38 +81,47 @@ def __init__(self, hilo: Hilo, device): @property def current_temperature(self): + """Return the current temperature.""" return self._device.current_temperature @property def target_temperature(self): + """Return the target temperature.""" return self._device.target_temperature @property def max_temp(self): + """Return the maximum temperature.""" return self._device.max_temp @property def min_temp(self): + """Return the minimum temperature.""" return self._device.min_temp def set_hvac_mode(self, hvac_mode): + """Set hvac mode.""" return @property def hvac_mode(self): + """Return hvac mode.""" return HVACMode.HEAT @property def hvac_action(self): + """Return the current hvac action.""" return self._device.hvac_action @property def icon(self): + """Return the icon to use in the frontend, based on hvac_action.""" if self._device.hvac_action == HVACAction.HEATING: return "mdi:radiator" return "mdi:radiator-disabled" async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" if ATTR_TEMPERATURE in kwargs: if self._hilo.challenge_lock: challenge = self._hilo._hass.states.get("sensor.defi_hilo") diff --git a/custom_components/hilo/config_flow.py b/custom_components/hilo/config_flow.py index d986a009..618ab087 100755 --- a/custom_components/hilo/config_flow.py +++ b/custom_components/hilo/config_flow.py @@ -154,7 +154,7 @@ class HiloOptionsFlowHandler(config_entries.OptionsFlow): """Handle a Hilo options flow.""" def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize""" + """Initialize.""" if AwesomeVersion(HAVERSION) < "2024.11.99": self.config_entry = config_entry else: diff --git a/custom_components/hilo/const.py b/custom_components/hilo/const.py index 2e1fac11..e61d0716 100755 --- a/custom_components/hilo/const.py +++ b/custom_components/hilo/const.py @@ -1,3 +1,5 @@ +"""Hilo integration constants.""" + import logging from homeassistant.components.utility_meter.const import DAILY diff --git a/custom_components/hilo/entity.py b/custom_components/hilo/entity.py index 72581e10..3d374f78 100644 --- a/custom_components/hilo/entity.py +++ b/custom_components/hilo/entity.py @@ -62,6 +62,7 @@ def __init__( @property def should_poll(self) -> bool: + """Return whether the entity should be polled.""" return False @property diff --git a/custom_components/hilo/light.py b/custom_components/hilo/light.py index 7ce07d82..acd38ea3 100644 --- a/custom_components/hilo/light.py +++ b/custom_components/hilo/light.py @@ -1,3 +1,5 @@ +"""Hilo Light platform integration.""" + from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR, LightEntity from homeassistant.components.light.const import ColorMode from homeassistant.config_entries import ConfigEntry @@ -15,6 +17,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: + """Set up Hilo light entities from a config entry.""" hilo = hass.data[DOMAIN][entry.entry_id] entities = [] @@ -26,7 +29,10 @@ async def async_setup_entry( class HiloLight(HiloEntity, LightEntity): + """Define a Hilo Light entity.""" + def __init__(self, hass: HomeAssistant, hilo: Hilo, device): + """Initialize the Hilo light entity.""" super().__init__(hilo, device=device, name=device.name) old_unique_id = f"{slugify(device.name)}-light" self._attr_unique_id = f"{slugify(device.identifier)}-light" @@ -44,18 +50,22 @@ def __init__(self, hass: HomeAssistant, hilo: Hilo, device): @property def brightness(self): + """Return the brightness of the light.""" return self._device.brightness @property def state(self): + """Return the state of the light.""" return self._device.state @property def is_on(self): + """Return whether the light is on.""" return self._device.get_value("is_on") @property def hs_color(self): + """Return the HS color.""" return (self._device.hue, self._device.saturation) @property @@ -80,11 +90,13 @@ def supported_color_modes(self) -> set: return color_modes async def async_turn_off(self, **kwargs): + """Turn off the light.""" LOG.info(f"{self._device._tag} Turning off") await self._device.set_attribute("is_on", False) self.async_schedule_update_ha_state(True) async def async_turn_on(self, **kwargs): + """Turn on the light.""" self._last_kwargs = kwargs await self._debounced_turn_on.async_call() diff --git a/custom_components/hilo/managers.py b/custom_components/hilo/managers.py index 2e91d5ea..b56e5a84 100644 --- a/custom_components/hilo/managers.py +++ b/custom_components/hilo/managers.py @@ -1,3 +1,5 @@ +"""Utility and Energy Manager classes for Hilo integration.""" + from collections import OrderedDict from datetime import timedelta @@ -15,9 +17,10 @@ class UtilityManager: - """Class that maps to the utility_meters""" + """Class that maps to the utility_meters.""" def __init__(self, hass, period, tariffs): + """Initialize the utility manager.""" self.tariffs = tariffs self.hass = hass self.period = period @@ -26,10 +29,12 @@ def __init__(self, hass, period, tariffs): self.new_entities = 0 def add_meter(self, entity, tariff_list, net_consumption=False): + """Add meter.""" self.add_meter_entity(entity, tariff_list) self.add_meter_config(entity, tariff_list, net_consumption) def add_meter_entity(self, entity, tariff_list): + """Add meter entity.""" if entity in self.hass.data.get("utility_meter_data", {}): LOG.debug("Entity %s is already in the utility meters", entity) return @@ -45,6 +50,7 @@ def add_meter_entity(self, entity, tariff_list): } def add_meter_config(self, entity, tariff_list, net_consumption): + """Add meter configuration.""" name = f"{entity}_{self.period}" LOG.debug( "Creating UtilityMeter config: %s %s (Net Consumption: %s)", @@ -68,6 +74,7 @@ def add_meter_config(self, entity, tariff_list, net_consumption): ) async def update(self, async_add_entities): + """Update the entities.""" LOG.debug("Setting up UtilityMeter entities %s", UTILITY_DOMAIN) if self.new_entities == 0: LOG.debug("No new entities, not setting up again") @@ -85,11 +92,15 @@ async def update(self, async_add_entities): class EnergyManager: + """Class that manages the energy dashboard configuration.""" + def __init__(self): + """Initialize the energy manager.""" self.updated = False @property def msg(self): + """Return the message payload for the energy manager.""" return { "energy_sources": self.src, "device_consumption": self.dev, @@ -97,6 +108,7 @@ def msg(self): @property def default_flows(self): + """Return the default grid flow configuration.""" return { "type": "grid", "flow_from": [], @@ -105,6 +117,7 @@ def default_flows(self): } async def init(self, hass, period): + """Initialize the energy manager.""" self.period = period self._manager = await async_get_manager(hass) data = self._manager.data or self._manager.default_preferences() @@ -116,6 +129,7 @@ async def init(self, hass, period): return self def add_flow_from(self, sensor, rate): + """Add grid source flow_from sensor.""" sensor = f"sensor.{sensor}" if any(d["stat_energy_from"] == sensor for d in self.src[0]["flow_from"]): return @@ -131,6 +145,7 @@ def add_flow_from(self, sensor, rate): self.src[0]["flow_from"].append(flow) def add_device(self, sensor): + """Add device consumption sensor.""" sensor = f"sensor.{sensor}" if any(d["stat_consumption"] == sensor for d in self.dev): return @@ -139,6 +154,7 @@ def add_device(self, sensor): self.dev.append({"stat_consumption": sensor}) def add_to_dashboard(self, entity, tariff_list): + """Add entity to the energy dashboard.""" for tarif in tariff_list: name = f"{entity}_{self.period}" if entity == HILO_ENERGY_TOTAL: @@ -147,6 +163,7 @@ def add_to_dashboard(self, entity, tariff_list): self.add_device(f"{name}_{tarif}") async def update(self): + """Push updates to the energy dashboard.""" if not self.updated: return LOG.debug("Pushing config to the energy dashboard") diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index 258fd8bb..a4a9e49a 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -97,6 +97,7 @@ def validate_tariff_list(tariff_config): def generate_entities_from_device(device, hilo, scan_interval): + """Generate the entities from the device description.""" entities = [] if device.type == "Gateway": entities.append( @@ -217,6 +218,7 @@ class BatterySensor(HiloEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo, device): + """Hilo battery sensor initialization.""" self._attr_name = f"{device.name} Battery" super().__init__(hilo, name=self._attr_name, device=device) old_unique_id = f"{slugify(device.name)}-battery" @@ -228,10 +230,12 @@ def __init__(self, hilo, device): @property def state(self): + """Return the battery level.""" return str(int(self._device.get_value("battery", 0))) @property def icon(self): + """Return the icon representing the battery level.""" if not self._device.available: return "mdi:lan-disconnect" level = round(int(self._device.get_value("battery", 0)) / 10) * 10 @@ -248,6 +252,7 @@ class Co2Sensor(HiloEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo, device): + """Hilo CO2 sensor initialization.""" self._attr_name = f"{device.name} CO2" super().__init__(hilo, name=self._attr_name, device=device) old_unique_id = f"{slugify(device.name)}-co2" @@ -259,10 +264,12 @@ def __init__(self, hilo, device): @property def state(self): + """Return the CO2 level.""" return str(int(self._device.get_value("co2", 0))) @property def icon(self): + """Return the icon representing the CO2 level.""" if not self._device.available: return "mdi:lan-disconnect" return "mdi:molecule-co2" @@ -279,6 +286,7 @@ class EnergySensor(IntegrationSensor): _attr_icon = "mdi:lightning-bolt" def __init__(self, hilo, device, hass): + """Hilo Energy sensor initialization.""" self._device = device self._attr_name = f"{device.name} Hilo Energy" old_unique_id = f"hilo_energy_{slugify(device.name)}" @@ -347,10 +355,12 @@ def __init__(self, hilo, device, hass): @property def unit_of_measurement(self): + """Return the unit of measurement.""" return self._attr_unit_of_measurement @property def suggested_display_precision(self): + """Return the suggested display precision.""" return self._attr_suggested_display_precision async def async_added_to_hass(self) -> None: @@ -366,6 +376,7 @@ class NoiseSensor(HiloEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo, device): + """Hilo Noise sensor initialization.""" self._attr_name = f"{device.name} Noise" super().__init__(hilo, name=self._attr_name, device=device) old_unique_id = f"{slugify(device.name)}-noise" @@ -377,10 +388,12 @@ def __init__(self, hilo, device): @property def state(self): + """Return the current noise level.""" return str(int(self._device.get_value("noise", 0))) @property def icon(self): + """Return the icon representing the noise level.""" if not self._device.available: return "mdi:lan-disconnect" if int(self._device.get_value("noise", 0)) > 0: @@ -408,10 +421,12 @@ def __init__(self, hilo: Hilo, device: HiloDevice) -> None: @property def state(self): + """Return the current power state.""" return str(int(self._device.get_value("power", 0))) @property def icon(self): + """Return the icon representing the power state.""" if not self._device.available: return "mdi:lan-disconnect" power = int(self._device.get_value("power", 0)) @@ -428,6 +443,7 @@ class TemperatureSensor(HiloEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo, device): + """Hilo Temperature sensor initialization.""" self._attr_name = f"{device.name} Temperature" super().__init__(hilo, name=self._attr_name, device=device) old_unique_id = f"{slugify(device.name)}-temperature" @@ -439,10 +455,12 @@ def __init__(self, hilo, device): @property def state(self): + """Return the current temperature.""" return str(float(self._device.get_value("current_temperature", 0))) @property def icon(self): + """Return the icon representing the current temperature.""" current_temperature = int(self._device.get_value("current_temperature", 0)) if not self._device.available: thermometer = "off" @@ -463,6 +481,7 @@ class TargetTemperatureSensor(HiloEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo, device): + """Hilo Target Temperature sensor initialization.""" self._attr_name = f"{device.name} Target Temperature" super().__init__(hilo, name=self._attr_name, device=device) old_unique_id = f"{slugify(device.name)}-target-temperature" @@ -474,10 +493,12 @@ def __init__(self, hilo, device): @property def state(self): + """Return the target temperature.""" return str(float(self._device.get_value("target_temperature", 0))) @property def icon(self): + """Return the icon representing the target temperature.""" target_temperature = int(self._device.get_value("target_temperature", 0)) if not self._device.available: thermometer = "off" @@ -498,6 +519,7 @@ class WifiStrengthSensor(HiloEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo, device): + """Hilo Wi-Fi strength sensor initialization.""" self._attr_name = f"{device.name} WifiStrength" super().__init__(hilo, name=self._attr_name, device=device) self._attr_unique_id = f"{slugify(device.name)}-wifistrength" @@ -505,27 +527,32 @@ def __init__(self, hilo, device): @property def state(self): + """Return the Wi-Fi signal strength.""" return process_wifi(self._device.get_value("wifi_status", 0)) @property def icon(self): + """Return the icon representing the Wi-Fi strength.""" if not self._device.available or self._device.get_value("wifi_status", 0) == 0: return "mdi:wifi-strength-off" return f"mdi:wifi-strength-{WIFI_STRENGTH[self.state]}" @property def extra_state_attributes(self): + """Return the Wi-Fi signal strength.""" return {"wifi_signal": self._device.get_value("wifi_status", 0)} class HiloNotificationSensor(HiloEntity, RestoreEntity, SensorEntity): """Hilo Notification sensor. + Its state will be the number of notification waiting in the Hilo app. Notifications only used for OneLink's alerts & Low-battery warnings. We should consider having this sensor enabled only if a smoke detector is in use. """ def __init__(self, hilo, device, scan_interval): + """Hilo Notification sensor initialization.""" self._attr_name = "Notifications Hilo" super().__init__(hilo, name=self._attr_name, device=device) old_unique_id = slugify(self._attr_name) @@ -543,6 +570,7 @@ def __init__(self, hilo, device, scan_interval): @property def state(self): + """Return the number of notifications.""" try: return int(self._state) except ValueError: @@ -550,6 +578,7 @@ def state(self): @property def icon(self): + """Return the icon based on the notification state.""" if not self._device.available: return "mdi:lan-disconnect" if self.state > 0: @@ -558,10 +587,12 @@ def icon(self): @property def should_poll(self): + """Enable polling.""" return True @property def extra_state_attributes(self): + """Return the notifications.""" return {"notifications": self._notifications} async def async_added_to_hass(self): @@ -594,6 +625,7 @@ async def _async_update(self): class HiloRewardSensor(HiloEntity, RestoreEntity, SensorEntity): """Hilo Reward sensor. + Its state will be either 0 or the total amount rewarded this season. """ @@ -602,6 +634,7 @@ class HiloRewardSensor(HiloEntity, RestoreEntity, SensorEntity): _entity_component_unrecorded_attributes = frozenset({"history"}) def __init__(self, hilo, device, scan_interval): + """Hilo Reward sensor initialization.""" self._attr_name = "Recompenses Hilo" # Check if currency is configured, set a default if not @@ -645,20 +678,24 @@ def __init__(self, hilo, device, scan_interval): @property def state(self): + """Return the total reward amount for the current season.""" return self._state @property def icon(self): + """Set the icon based on the current state.""" if not self._device.available: return "mdi:lan-disconnect" return "mdi:cash-plus" @property def should_poll(self): + """Enable polling.""" return True @property def extra_state_attributes(self): + """Return the history attributes.""" return {"history": self._history} async def async_added_to_hass(self): @@ -670,6 +707,7 @@ async def async_added_to_hass(self): self._state = last_state.state async def handle_challenge_details_update(self, challenge): + """Handle challenge details update from websocket.""" LOG.debug("UPDATING challenge in reward: %s", challenge) # We're getting events but didn't request any, do not process them @@ -731,7 +769,7 @@ async def _async_update(self): total = sum( event["reward"] for event in season_data["events"] - if not event.get("isPreseasonEvent") + if "reward" in event and not event.get("isPreseasonEvent") ) season_data["totalReward"] = total @@ -822,6 +860,7 @@ async def _save_history(self): class HiloChallengeSensor(HiloEntity, SensorEntity): """Hilo challenge sensor. + Its state will be either: - off: no ongoing or scheduled challenge - scheduled: A challenge is scheduled, details in the next_events @@ -834,6 +873,7 @@ class HiloChallengeSensor(HiloEntity, SensorEntity): """ def __init__(self, hilo, device, scan_interval): + """Hilo Challenge sensor initialization.""" self._attr_name = "Defi Hilo" self._attr_device_class = SensorDeviceClass.ENUM self._attr_options = [ @@ -1019,6 +1059,7 @@ def state(self): @property def icon(self): + """Set the icon based on the current state.""" if not self._device.available: return "mdi:lan-disconnect" if self.state == "appreciation": @@ -1039,11 +1080,12 @@ def icon(self): @property def should_poll(self): - """No need to poll with websockets. Polling to update allowed_wh in pre_heat phrase and consumption in reduction phase""" + """Don't poll with websockets. Poll to update allowed_wh in pre_heat phrase and consumption in reduction phase.""" return self.state in ["recovery", "reduction", "pre_heat"] @property def extra_state_attributes(self): + """Return the next events attribute.""" return {"next_events": self._next_events} async def async_added_to_hass(self): @@ -1051,7 +1093,7 @@ async def async_added_to_hass(self): await super().async_added_to_hass() async def _async_update(self): - """This method can be kept for fallback but shouldn't be needed with websockets.""" + """Update fallback, but not needed with websockets.""" for event_id in self._events: event = self._events.get(event_id) if event.should_check_for_allowed_wh(): @@ -1064,12 +1106,15 @@ async def _async_update(self): class DeviceSensor(HiloEntity, SensorEntity): - """Devices like the gateway or Smoke Detectors don't have many attributes, + """Simple device entity. + + Devices like the gateway or Smoke Detectors don't have many attributes, except for the "disconnected" attribute. These entities are monitoring this state. """ def __init__(self, hilo, device): + """Initialize.""" self._attr_name = device.name super().__init__(hilo, name=self._attr_name, device=device) old_unique_id = slugify(device.name) @@ -1081,14 +1126,17 @@ def __init__(self, hilo, device): @property def state(self): + """Return the connection state.""" return "on" if self._device.available else "off" @property def extra_state_attributes(self): + """Return the extra state attributes.""" return {k: self._device.get_value(k) for k in self._device.attributes} @property def icon(self): + """Set the icon based on the connection state.""" if not self._device.available: return "mdi:lan-disconnect" if self.state == "off": @@ -1097,7 +1145,7 @@ def icon(self): class HiloCostSensor(HiloEntity, SensorEntity): - """This sensor generates cost entities""" + """This sensor generates cost entities.""" _attr_device_class = SensorDeviceClass.MONETARY _attr_native_unit_of_measurement = ( @@ -1107,6 +1155,7 @@ class HiloCostSensor(HiloEntity, SensorEntity): _attr_icon = "mdi:cash" def __init__(self, hilo, name, plan_name, amount=0): + """Initialize.""" for d in hilo.devices.all: if d.type == "Gateway": device = d @@ -1153,14 +1202,17 @@ def _handle_state_change(self, event): @property def state(self): + """Return the cost.""" return self._cost @property def should_poll(self) -> bool: + """Disable polling.""" return False @property def extra_state_attributes(self): + """Return the cost sensor attributes.""" return { "Cost": self._cost, "Plan": self.plan_name, @@ -1172,12 +1224,14 @@ async def async_added_to_hass(self): await super().async_added_to_hass() async def async_update(self): + """Update the state.""" self._last_update = dt_util.utcnow() return super().async_update() class HiloOutdoorTempSensor(HiloEntity, SensorEntity): """Hilo outdoor temperature sensor. + Its state will be the current outdoor weather as reported by the Hilo App """ @@ -1186,6 +1240,7 @@ class HiloOutdoorTempSensor(HiloEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo, device, scan_interval): + """Initialize.""" self._attr_name = "Outdoor Weather Hilo" super().__init__(hilo, name=self._attr_name, device=device) self._attr_unique_id = ( @@ -1199,6 +1254,7 @@ def __init__(self, hilo, device, scan_interval): @property def state(self): + """Return the current outdoor temperature.""" try: return int(self._state) except ValueError: @@ -1206,6 +1262,7 @@ def state(self): @property def icon(self): + """Set the icon based on weather condition.""" condition = self._weather.get("condition", "").lower() LOG.debug("Current condition: %s", condition) if not condition: @@ -1214,10 +1271,12 @@ def icon(self): @property def should_poll(self): + """Poll needed to update weather data.""" return True @property def extra_state_attributes(self): + """Add weather attributes.""" LOG.debug("Adding weather %s", self._weather) return { key: self._weather[key] diff --git a/custom_components/hilo/switch.py b/custom_components/hilo/switch.py index 2e106324..09d29391 100644 --- a/custom_components/hilo/switch.py +++ b/custom_components/hilo/switch.py @@ -1,3 +1,5 @@ +"""Support for Hilo switches.""" + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -14,6 +16,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: + """Set up Hilo switches based on a config entry.""" hilo = hass.data[DOMAIN][entry.entry_id] entities = [] @@ -25,7 +28,10 @@ async def async_setup_entry( class HiloSwitch(HiloEntity, SwitchEntity): + """Representation of a Hilo Switch.""" + def __init__(self, hilo: Hilo, device: Switch): + """Initialize the switch.""" super().__init__(hilo, device=device, name=device.name) old_unique_id = f"{slugify(device.name)}-switch" self._attr_unique_id = f"{slugify(device.identifier)}-switch" @@ -36,10 +42,12 @@ def __init__(self, hilo: Hilo, device: Switch): @property def state(self): + """Return the state of the switch.""" return self._device.state @property def icon(self): + """Set the icon based on the switch state.""" if not self._device.available: return "mdi:lan-disconnect" if self.state == "on": @@ -48,14 +56,17 @@ def icon(self): @property def is_on(self): + """Return true if the switch is on.""" return self._device.get_value("is_on") async def async_turn_off(self, **kwargs): + """Turn the switch off.""" LOG.info(f"{self._device._tag} Turning off") await self._device.set_attribute("is_on", False) self.async_schedule_update_ha_state(True) async def async_turn_on(self, **kwargs): + """Turn the switch on.""" LOG.info(f"{self._device._tag} Turning on") await self._device.set_attribute("is_on", True) self.async_schedule_update_ha_state(True)