Skip to content

Commit

Permalink
Merge branch 'master' into alexa-media-player-v4.10.3
Browse files Browse the repository at this point in the history
  • Loading branch information
kylegordon committed Jul 7, 2024
2 parents 36f313e + 1731394 commit 67de930
Show file tree
Hide file tree
Showing 22 changed files with 1,041 additions and 181 deletions.
5 changes: 3 additions & 2 deletions custom_components/mypyllant/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import logging
from datetime import datetime as dt, timedelta
from datetime import datetime as dt, timedelta, datetime
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
Expand Down Expand Up @@ -133,6 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

async def handle_export(call: ServiceCall) -> ServiceResponse:
_LOGGER.debug("Exporting data with params %s", call.data)
return {
"export": await export.main(
user=username,
Expand Down Expand Up @@ -163,7 +164,7 @@ async def handle_report(call: ServiceCall) -> ServiceResponse:
password=password,
brand=brand,
country=country,
year=call.data.get("year"),
year=int(call.data.get("year", datetime.now().year)),
write_results=False,
)
}
Expand Down
82 changes: 79 additions & 3 deletions custom_components/mypyllant/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from . import SystemCoordinator
from .const import DOMAIN
from .utils import EntityList
from .utils import EntityList, ZoneCoordinatorEntity

_LOGGER = logging.getLogger(__name__)

Expand All @@ -39,12 +39,20 @@ async def async_setup_entry(
sensors.append(lambda: ControlOnline(index, coordinator))
sensors.append(lambda: FirmwareUpdateRequired(index, coordinator))
sensors.append(lambda: FirmwareUpdateEnabled(index, coordinator))
for circuit_index, circuit in enumerate(system.circuits):
if system.eebus:
sensors.append(lambda: EebusEnabled(index, coordinator))
sensors.append(lambda: EebusCapable(index, coordinator))
for circuit_index, _ in enumerate(system.circuits):
sensors.append(
lambda: CircuitIsCoolingAllowed(index, circuit_index, coordinator)
)
for zone_index, zone in enumerate(system.zones):
if zone.is_manual_cooling_active is not None:
sensors.append(
lambda: ZoneIsManualCoolingActive(index, zone_index, coordinator)
)

async_add_entities(sensors)
async_add_entities(sensors) # type: ignore


class SystemControlEntity(CoordinatorEntity, BinarySensorEntity):
Expand Down Expand Up @@ -180,6 +188,60 @@ def unique_id(self) -> str:
return f"{DOMAIN}_{self.id_infix}_firmware_update_enabled"


class EebusCapable(SystemControlEntity):
_attr_icon = "mdi:check-network"

def __init__(
self,
system_index: int,
coordinator: SystemCoordinator,
):
super().__init__(system_index, coordinator)

@property
def is_on(self) -> bool | None:
return (
self.system.eebus.get("spine_capable", False)
if self.system.eebus
else False
)

@property
def name(self) -> str:
return f"{self.name_prefix} EEBUS Capable"

@property
def unique_id(self) -> str:
return f"{DOMAIN}_{self.id_infix}_eebus_capable"


class EebusEnabled(SystemControlEntity):
_attr_icon = "mdi:check-network"

def __init__(
self,
system_index: int,
coordinator: SystemCoordinator,
):
super().__init__(system_index, coordinator)

@property
def is_on(self) -> bool | None:
return (
self.system.eebus.get("spine_enabled", False)
if self.system.eebus
else False
)

@property
def name(self) -> str:
return f"{self.name_prefix} EEBUS Enabled"

@property
def unique_id(self) -> str:
return f"{DOMAIN}_{self.id_infix}_eebus_enabled"


class CircuitEntity(CoordinatorEntity, BinarySensorEntity):
def __init__(
self,
Expand Down Expand Up @@ -236,3 +298,17 @@ def name(self) -> str:
@property
def unique_id(self) -> str:
return f"{DOMAIN} {self.id_infix}_cooling_allowed"


class ZoneIsManualCoolingActive(ZoneCoordinatorEntity, BinarySensorEntity):
@property
def is_on(self) -> bool | None:
return self.zone.is_manual_cooling_active

@property
def name(self) -> str:
return f"{self.name_prefix} Manual Cooling Active"

@property
def unique_id(self) -> str:
return f"{DOMAIN} {self.id_infix}_manual_cooling_active"
102 changes: 81 additions & 21 deletions custom_components/mypyllant/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import copy
import datetime
import logging
import re
from abc import ABC, abstractmethod
from typing import Any

from homeassistant.components.calendar import (
Expand All @@ -24,8 +26,9 @@
ZoneTimeProgram,
RoomTimeProgramDay,
RoomTimeProgram,
System,
)
from myPyllant.enums import ZoneTimeProgramType
from myPyllant.enums import ZoneOperatingType

from . import SystemCoordinator
from .const import DOMAIN, WEEKDAYS_TO_RFC5545, RFC5545_TO_WEEKDAYS
Expand Down Expand Up @@ -57,6 +60,10 @@ async def async_setup_entry(
sensors.append(
lambda: ZoneHeatingCalendar(index, zone_index, coordinator)
)
if zone.cooling and zone.cooling.time_program_cooling:
sensors.append(
lambda: ZoneCoolingCalendar(index, zone_index, coordinator)
)
for dhw_index, dhw in enumerate(system.domestic_hot_water):
sensors.append(
lambda: DomesticHotWaterCalendar(index, dhw_index, coordinator)
Expand All @@ -70,10 +77,10 @@ async def async_setup_entry(
sensors.append(
lambda: AmbisenseCalendar(index, room.room_index, coordinator)
)
async_add_entities(sensors)
async_add_entities(sensors) # type: ignore


class BaseCalendarEntity(CalendarEntity):
class BaseCalendarEntity(CalendarEntity, ABC):
_attr_supported_features = (
CalendarEntityFeature.CREATE_EVENT
| CalendarEntityFeature.DELETE_EVENT
Expand Down Expand Up @@ -104,18 +111,22 @@ def _get_rrule(self, time_program_day: BaseTimeProgramDay) -> str:
matching_weekdays = self.time_program.matching_weekdays(time_program_day)
return f"FREQ=WEEKLY;INTERVAL=1;BYDAY={','.join([WEEKDAYS_TO_RFC5545[d] for d in matching_weekdays])}"

def _get_weekdays_from_rrule(self, rrule: str) -> list[str]:
@staticmethod
def _get_weekdays_from_rrule(rrule: str) -> list[str]:
by_day = [p for p in rrule.split(";") if p.startswith("BYDAY=")][0].replace(
"BYDAY=", ""
)
return [RFC5545_TO_WEEKDAYS[d] for d in by_day.split(",")]

def get_setpoint_from_summary(self, summary: str):
@staticmethod
def get_setpoint_from_summary(summary: str) -> float:
"""
Extracts a float temperature value from a string such as "Heating to 21.0°C on Home Zone 1 (Circuit 0)"
"""
match = re.search(r"([0-9.,]+)°?C?", summary)
try:
if " " in summary:
summary = summary.split(" ")[0]
return float(summary.replace("°C", ""))
except ValueError:
return float(match.group(1).replace(",", ".")) # type: ignore
except (ValueError, AttributeError):
raise HomeAssistantError("Invalid setpoint, use format '21.5°C' in Summary")

def _check_overlap(self):
Expand All @@ -135,6 +146,11 @@ def build_event(
async def update_time_program(self):
raise NotImplementedError

@property
@abstractmethod
def system(self) -> System:
pass

@property
def event(self) -> CalendarEvent | None:
start = datetime.datetime.now(self.system.timezone)
Expand Down Expand Up @@ -254,8 +270,8 @@ async def async_update_event(
await self.update_time_program()


class ZoneHeatingCalendar(BaseCalendarEntity, ZoneCoordinatorEntity):
_attr_icon = "mdi:thermometer-auto"
class ZoneHeatingCalendar(ZoneCoordinatorEntity, BaseCalendarEntity):
_attr_icon = "mdi:home-thermometer"
_has_setpoint = True

@property
Expand All @@ -264,18 +280,23 @@ def time_program(self) -> ZoneTimeProgram:

@property
def name(self) -> str:
return f"{self.name_prefix} Heating Schedule"
return self.name_prefix

def _get_calendar_id_prefix(self):
return f"zone_heating_{self.zone.index}"

def _get_setpoint(self, time_program_day: ZoneTimeProgramDay):
if time_program_day.setpoint:
return time_program_day.setpoint
return self.zone.desired_room_temperature_setpoint_heating

def build_event(
self,
time_program_day: ZoneTimeProgramDay,
start: datetime.datetime,
end: datetime.datetime,
):
summary = f"{time_program_day.setpoint}°C on {self.name}"
summary = f"Heating to {self._get_setpoint(time_program_day)}°C on {self.name}"
return CalendarEvent(
summary=summary,
start=start,
Expand All @@ -288,21 +309,60 @@ def build_event(

async def update_time_program(self):
await self.coordinator.api.set_zone_time_program(
self.zone, str(ZoneTimeProgramType.HEATING), self.time_program
self.zone, str(ZoneOperatingType.HEATING), self.time_program
)
await self.coordinator.async_request_refresh_delayed()


class ZoneCoolingCalendar(ZoneCoordinatorEntity, BaseCalendarEntity):
_attr_icon = "mdi:snowflake-thermometer"
_has_setpoint = True

@property
def time_program(self) -> ZoneTimeProgram:
return self.zone.cooling.time_program_cooling # type: ignore

@property
def name(self) -> str:
return self.name_prefix

def _get_calendar_id_prefix(self):
return f"zone_cooling_{self.zone.index}"

def build_event(
self,
time_program_day: ZoneTimeProgramDay,
start: datetime.datetime,
end: datetime.datetime,
):
summary = f"Cooling to {self.zone.desired_room_temperature_setpoint_cooling}°C on {self.name}"
return CalendarEvent(
summary=summary,
start=start,
end=end,
description="You can change the start time, end time, or weekdays. Temperature is the same for all slots.",
uid=self._get_uid(time_program_day, start),
rrule=self._get_rrule(time_program_day),
recurrence_id=self._get_recurrence_id(time_program_day),
)

async def update_time_program(self):
await self.coordinator.api.set_zone_time_program(
self.zone, str(ZoneOperatingType.COOLING), self.time_program
)
await self.coordinator.async_request_refresh_delayed()


class DomesticHotWaterCalendar(BaseCalendarEntity, DomesticHotWaterCoordinatorEntity):
_attr_icon = "mdi:water-boiler-auto"
class DomesticHotWaterCalendar(DomesticHotWaterCoordinatorEntity, BaseCalendarEntity):
_attr_icon = "mdi:water-thermometer"

@property
def time_program(self) -> DHWTimeProgram:
return self.domestic_hot_water.time_program_dhw

@property
def name(self) -> str:
return f"{self.name_prefix} Schedule"
return self.name_prefix

def _get_calendar_id_prefix(self):
return f"dhw_{self.domestic_hot_water.index}"
Expand All @@ -313,7 +373,7 @@ def build_event(
start: datetime.datetime,
end: datetime.datetime,
):
summary = f"{self.domestic_hot_water.tapping_setpoint}°C on {self.name}"
summary = f"Heating Water to {self.domestic_hot_water.tapping_setpoint}°C on {self.name}"
return CalendarEvent(
summary=summary,
start=start,
Expand All @@ -332,7 +392,7 @@ async def update_time_program(self):


class DomesticHotWaterCirculationCalendar(
BaseCalendarEntity, DomesticHotWaterCoordinatorEntity
DomesticHotWaterCoordinatorEntity, BaseCalendarEntity
):
_attr_icon = "mdi:pump"

Expand All @@ -342,7 +402,7 @@ def time_program(self) -> DHWTimeProgram:

@property
def name(self) -> str:
return f"{self.name_prefix} Circulation Schedule"
return f"Circulating Water in {self.name_prefix}"

def _get_calendar_id_prefix(self):
return f"dhw_circulation_{self.domestic_hot_water.index}"
Expand Down Expand Up @@ -371,7 +431,7 @@ async def update_time_program(self):
await self.coordinator.async_request_refresh_delayed()


class AmbisenseCalendar(BaseCalendarEntity, AmbisenseCoordinatorEntity):
class AmbisenseCalendar(AmbisenseCoordinatorEntity, BaseCalendarEntity):
_attr_icon = "mdi:thermometer-auto"
_has_setpoint = True

Expand Down
Loading

0 comments on commit 67de930

Please sign in to comment.