Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
pull_request:

env:
DEFAULT_PYTHON: 3.9
DEFAULT_PYTHON: 3.13

jobs:
pre-commit:
Expand Down
18 changes: 9 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
repos:
- repo: https://github.com/PyCQA/bandit
rev: '1.7.9'
rev: '1.9.2'
hooks:
- id: bandit
args:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -29,15 +29,15 @@ 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:
- flake8-docstrings==1.5.0
- 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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
44 changes: 28 additions & 16 deletions custom_components/hilo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -519,15 +522,15 @@ 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
)

@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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -852,14 +859,15 @@ 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()

if self.track_unknown_sources:
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")
Expand Down Expand Up @@ -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:
Expand All @@ -913,20 +922,21 @@ 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
)
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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))
16 changes: 16 additions & 0 deletions custom_components/hilo/climate.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Support for Hilo Climate entities."""

from datetime import datetime, timedelta

from homeassistant.components.climate import ClimateEntity
Expand All @@ -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]
Expand All @@ -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:
Expand 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"
Expand All @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hilo/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions custom_components/hilo/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Hilo integration constants."""

import logging

from homeassistant.components.utility_meter.const import DAILY
Expand Down
Loading