Skip to content

Commit

Permalink
V1 config + testus + test int ok
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Marc Collin committed Nov 11, 2024
1 parent f442eab commit dd5c5e0
Show file tree
Hide file tree
Showing 9 changed files with 399 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .devcontainer/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ solar_optimizer:
activation_service: "input_boolean/turn_on"
deactivation_service: "input_boolean/turn_off"
max_on_time_per_day_min: 10
min_on_time_per_day_min: 5
offpeak_time: "19:48"
- name: "Equipement B"
entity_id: "input_boolean.fake_device_b"
power_max: 500
Expand All @@ -170,6 +172,8 @@ solar_optimizer:
action_mode: "service_call"
activation_service: "input_boolean/turn_on"
deactivation_service: "input_boolean/turn_off"
min_on_time_per_day_min: 15
offpeak_time: "19:50"
- name: "Equipement C"
entity_id: "input_boolean.fake_device_c"
power_max: 800
Expand Down
15 changes: 14 additions & 1 deletion custom_components/solar_optimizer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""Initialisation du package de l'intégration HACS Tuto"""
import logging
import asyncio
import re
import voluptuous as vol
from voluptuous.error import Invalid

from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component


from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import ConfigType
import homeassistant.helpers.config_validation as cv
Expand All @@ -29,13 +32,21 @@
# from homeassistant.helpers.entity_component import EntityComponent


from .const import DOMAIN, PLATFORMS
from .const import DOMAIN, PLATFORMS, SERVICE_RESET_ON_TIME
from .coordinator import SolarOptimizerCoordinator

# from .input_boolean import async_setup_entry as async_setup_entry_input_boolean

_LOGGER = logging.getLogger(__name__)


def validate_time_format(value):
"""check is a string have format "hh:mm" with hh between 00 and 23 and mm between 00 et 59"""
if not re.match(r"^(?:[01]\d|2[0-3]):[0-5]\d$", value):
raise Invalid("The time value should be formatted like 'hh:mm'")
return value


CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
Expand Down Expand Up @@ -90,6 +101,8 @@
"battery_soc_threshold", default=0
): vol.Coerce(float),
vol.Optional("max_on_time_per_day_min"): vol.Coerce(int),
vol.Optional("min_on_time_per_day_min"): vol.Coerce(int),
vol.Optional("offpeak_time"): validate_time_format,
}
]
),
Expand Down
2 changes: 2 additions & 0 deletions custom_components/solar_optimizer/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
INTEGRATION_MODEL = "Solar Optimizer"
DEVICE_MANUFACTURER = "JM. COLLIN"

SERVICE_RESET_ON_TIME = "reset_on_time"


def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
Expand Down
7 changes: 5 additions & 2 deletions custom_components/solar_optimizer/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,14 @@ async def _async_update_data(self):
if not device:
continue
is_active = device.is_active
if is_active and not state:
should_force_offpeak = device.should_be_forced_offpeak
if should_force_offpeak:
_LOGGER.debug("%s - we should force %s name", self, name)
if is_active and not state and not should_force_offpeak:
_LOGGER.debug("Extinction de %s", name)
should_log = True
await device.deactivate()
elif not is_active and state:
elif not is_active and (state or should_force_offpeak):
_LOGGER.debug("Allumage de %s", name)
should_log = True
await device.activate(requested_power)
Expand Down
42 changes: 41 additions & 1 deletion custom_components/solar_optimizer/managed_device.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
""" A ManagedDevice represent a device than can be managed by the optimisatiion algorithm"""
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, time

from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
Expand Down Expand Up @@ -126,6 +126,8 @@ class ManagedDevice:
_battery_soc_threshold: float
_max_on_time_per_day_sec: int
_on_time_sec: int
_min_on_time_per_day_sec: int
_offpeak_time: time

def __init__(self, hass: HomeAssistant, device_config):
"""Initialize a manageable device"""
Expand Down Expand Up @@ -186,13 +188,33 @@ def __init__(self, hass: HomeAssistant, device_config):
)
self._on_time_sec = 0

self._min_on_time_per_day_sec = (
int(device_config.get("min_on_time_per_day_min") or 0) * 60
)

self._offpeak_time = datetime.strptime(
device_config.get("offpeak_time") or "23:59", "%H:%M"
).time()

if self.is_active:
self._requested_power = self._current_power = (
self._power_max if self._can_change_power else self._power_min
)

self._enable = True

# Some checks
# min_on_time_per_day_sec requires an offpeak_time
if self._min_on_time_per_day_sec > 0 and self._offpeak_time == time(23, 59):
msg = f"configuration of device ${self.name} is incorrect. min_on_time_per_day_sec requires offpeak_time value"
_LOGGER.error("%s - %s", self, msg)
raise ConfigurationError(msg)

if self._min_on_time_per_day_sec > self._max_on_time_per_day_sec:
msg = f"configuration of device ${self.name} is incorrect. min_on_time_per_day_sec should < max_on_time_per_day_sec"
_LOGGER.error("%s - %s", self, msg)
raise ConfigurationError(msg)

async def _apply_action(self, action_type: str, requested_power=None):
"""Apply an action to a managed device.
This method is a generical method for activate, deactivate, change_requested_power
Expand Down Expand Up @@ -390,6 +412,14 @@ def is_usable(self) -> bool:

return result

@property
def should_be_forced_offpeak(self) -> bool:
"""True is we are offpeak and the max_on_time is not exceeded"""
return (
self.now.time() >= self._offpeak_time
and self._on_time_sec < self._max_on_time_per_day_sec
)

@property
def is_waiting(self):
"""A device is waiting if the device is waiting for the end of its cycle"""
Expand Down Expand Up @@ -485,6 +515,16 @@ def max_on_time_per_day_sec(self) -> int:
"""The max_on_time_per_day_sec configured"""
return self._max_on_time_per_day_sec

@property
def min_on_time_per_day_sec(self) -> int:
"""The min_on_time_per_day_sec configured"""
return self._min_on_time_per_day_sec

@property
def offpeak_time(self) -> int:
"""The offpeak_time configured"""
return self._offpeak_time

@property
def battery_soc(self) -> int:
"""The battery soc"""
Expand Down
20 changes: 20 additions & 0 deletions custom_components/solar_optimizer/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from homeassistant.core import callback, HomeAssistant, Event, State
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers import entity_platform
from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
Expand All @@ -36,6 +37,7 @@
name_to_unique_id,
DEVICE_MODEL,
seconds_to_hms,
SERVICE_RESET_ON_TIME,
)
from .coordinator import SolarOptimizerCoordinator

Expand Down Expand Up @@ -71,6 +73,14 @@ async def async_setup_entry(

await coordinator.configure(entry)

# Add services
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_RESET_ON_TIME,
{},
"service_reset_on_time",
)


class SolarOptimizerSensorEntity(CoordinatorEntity, SensorEntity):
"""The entity holding the algorithm calculation"""
Expand Down Expand Up @@ -339,3 +349,13 @@ def last_datetime_on(self) -> datetime | None:
def get_attr_extra_state_attributes(self):
"""Get the extra state attributes for the entity"""
return self._attr_extra_state_attributes

async def service_reset_on_time(self):
"""Called by a service call:
service: sensor.reset_on_time
data:
target:
entity_id: solar_optimizer.on_time_today_solar_optimizer_<device name>
"""
_LOGGER.info("%s - Calling service_reset_on_time", self)
await self._on_midnight()
10 changes: 9 additions & 1 deletion custom_components/solar_optimizer/services.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
reload:
name: Reload
description: Reload Solar Optimizer configuration
description: Reload Solar Optimizer configuration

reset_on_time:
name: Reset on time
description: Reset all on time for all devices
target:
entity:
integration: solar_optimizer
fields:
56 changes: 56 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,59 @@ async def init_solar_optimizer_entry(hass):
await hass.async_block_till_done()

assert entry.state is ConfigEntryState.LOADED


@pytest.fixture(name="config_2_devices_min_on_time_ok")
def define_config_2_devices_min_on_time_ok():
"""Define a configuration with 2 devices. One with min_on_time and offpeak, the other without"""

return {
"solar_optimizer": {
"algorithm": {
"initial_temp": 1000,
"min_temp": 0.1,
"cooling_factor": 0.95,
"max_iteration_number": 1000,
},
"devices": [
{
"name": "Equipement A",
"entity_id": "input_boolean.fake_device_a",
"power_max": 1000,
"check_usable_template": "{{ True }}",
"duration_min": 2,
"duration_stop_min": 1,
"action_mode": "service_call",
"activation_service": "input_boolean/turn_on",
"deactivation_service": "input_boolean/turn_off",
"battery_soc_threshold": 30,
"max_on_time_per_day_min": 10,
"min_on_time_per_day_min": 5,
"offpeak_time": "23:00",
},
{
"name": "Equipement B",
"entity_id": "input_boolean.fake_device_b",
"power_max": 2000,
"check_usable_template": "{{ False }}",
"duration_min": 1,
"duration_stop_min": 2,
"duration_power_min": 3,
"action_mode": "service_call",
"activation_service": "input_boolean/turn_on",
"deactivation_service": "input_boolean/turn_off",
},
],
}
}


@pytest.fixture(name="init_solar_optimizer_with_2_devices_min_on_time_ok")
async def init_solar_optimizer_with_2_devices_min_on_time_ok(
hass, config_2_devices_min_on_time_ok
) -> SolarOptimizerCoordinator:
"""Initialization of Solar Optimizer with 2 managed device"""
await async_setup_component(
hass, "solar_optimizer", config_2_devices_min_on_time_ok
)
return hass.data[DOMAIN]["coordinator"]
Loading

0 comments on commit dd5c5e0

Please sign in to comment.