From ca47fe285b55687d603b5e6f0a2787a249d92604 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 20 Feb 2024 01:03:39 +0100 Subject: [PATCH 01/97] Initial commit for support of all schedule types, and fix for historical energy data --- src/pyatmo/account.py | 4 +- src/pyatmo/const.py | 34 +++++++++ src/pyatmo/exceptions.py | 6 ++ src/pyatmo/home.py | 66 ++++++++++++----- src/pyatmo/modules/module.py | 136 +++++++++++++++++++---------------- src/pyatmo/schedule.py | 117 ++++++++++++++++++++++++++++-- tests/test_home.py | 6 ++ 7 files changed, 285 insertions(+), 84 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 2986fcbb..fe1919d1 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -15,11 +15,11 @@ GETSTATIONDATA_ENDPOINT, HOME, SETSTATE_ENDPOINT, - RawData, + RawData, MeasureInterval, ) from pyatmo.helpers import extract_raw_data from pyatmo.home import Home -from pyatmo.modules.module import MeasureInterval, Module +from pyatmo.modules.module import Module if TYPE_CHECKING: from pyatmo.auth import AbstractAsyncAuth diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index fccbd6a6..52d0d14a 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -1,6 +1,7 @@ """Common constants.""" from __future__ import annotations +from enum import Enum from typing import Any ERRORS: dict[int, str] = { @@ -101,3 +102,36 @@ ACCESSORY_WIND_TIME_TYPE = "wind_timeutc" ACCESSORY_GUST_STRENGTH_TYPE = "gust_strength" ACCESSORY_GUST_ANGLE_TYPE = "gust_angle" + +SCHEDULE_TYPE_THERM = "therm" +SCHEDULE_TYPE_EVENT = "event" +SCHEDULE_TYPE_ELECTRICITY = "electricity" +SCHEDULE_TYPE_COOLING = "cooling" + + +class MeasureType(Enum): + """Measure type.""" + + BOILERON = "boileron" + BOILEROFF = "boileroff" + SUM_BOILER_ON = "sum_boiler_on" + SUM_BOILER_OFF = "sum_boiler_off" + SUM_ENERGY_ELEC = "sum_energy_elec" + SUM_ENERGY_ELEC_BASIC = "sum_energy_elec$0" + SUM_ENERGY_ELEC_PEAK = "sum_energy_elec$1" + SUM_ENERGY_ELEC_OFF_PEAK = "sum_energy_elec$2" + SUM_ENERGY_PRICE = "sum_energy_price" + SUM_ENERGY_PRICE_BASIC = "sum_energy_price$0" + SUM_ENERGY_PRICE_PEAK = "sum_energy_price$1" + SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_price$2" + + +class MeasureInterval(Enum): + """Measure interval.""" + + HALF_HOUR = "30min" + HOUR = "1hour" + THREE_HOURS = "3hours" + DAY = "1day" + WEEK = "1week" + MONTH = "1month" diff --git a/src/pyatmo/exceptions.py b/src/pyatmo/exceptions.py index 4ed5f120..146452e0 100644 --- a/src/pyatmo/exceptions.py +++ b/src/pyatmo/exceptions.py @@ -41,3 +41,9 @@ class InvalidState(Exception): """Raised when an invalid state is encountered.""" pass + + +class InvalidHistoryFromAPI(Exception): + """Raised when an invalid state is encountered.""" + + pass \ No newline at end of file diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 3828ef75..2f64110f 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -16,14 +16,14 @@ SETTHERMMODE_ENDPOINT, SWITCHHOMESCHEDULE_ENDPOINT, SYNCHOMESCHEDULE_ENDPOINT, - RawData, + RawData, SCHEDULE_TYPE_THERM, SCHEDULE_TYPE_ELECTRICITY, MeasureType, ) from pyatmo.event import Event from pyatmo.exceptions import InvalidSchedule, InvalidState, NoSchedule from pyatmo.modules import Module from pyatmo.person import Person from pyatmo.room import Room -from pyatmo.schedule import Schedule +from pyatmo.schedule import Schedule, schedule_factory, ThermSchedule if TYPE_CHECKING: from pyatmo.auth import AbstractAsyncAuth @@ -39,9 +39,11 @@ class Home: name: str rooms: dict[str, Room] modules: dict[str, Module] - schedules: dict[str, Schedule] + schedules: dict[str, ThermSchedule] #for compatibility should diseappear + all_schedules: dict[dict[str, str, Schedule]] persons: dict[str, Person] events: dict[str, Event] + energy_endpoints: list[str] def __init__(self, auth: AbstractAsyncAuth, raw_data: RawData) -> None: """Initialize a Netatmo home instance.""" @@ -61,15 +63,38 @@ def __init__(self, auth: AbstractAsyncAuth, raw_data: RawData) -> None: ) for room in raw_data.get("rooms", []) } - self.schedules = { - s["id"]: Schedule(home=self, raw_data=s) - for s in raw_data.get(SCHEDULES, []) - } + self._handle_schedules(raw_data.get(SCHEDULES, [])) self.persons = { s["id"]: Person(home=self, raw_data=s) for s in raw_data.get("persons", []) } self.events = {} + def _handle_schedules(self, raw_data): + + schedules = {} + + for s in raw_data: + #strange but Energy plan are stored in schedules, we should handle this one differently + sched, type = schedule_factory(home=self, raw_data=s) + if type not in schedules: + schedules[type] = {} + schedules[type][s["id"]] = sched + + self.schedules = schedules.get(SCHEDULE_TYPE_THERM, {}) + self.all_schedules = schedules + + nrj_schedule = next(iter(schedules.get(SCHEDULE_TYPE_ELECTRICITY, {}).values()), None) + + type_tariff = None + if nrj_schedule is not None: + type_tariff = nrj_schedule.tariff_option + + self.energy_endpoints = {"basic": [MeasureType.SUM_ENERGY_ELEC_BASIC.value], + "peak_and_off_peak": [MeasureType.SUM_ENERGY_ELEC_PEAK.value, MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value] + }.get(type_tariff, [MeasureType.SUM_ENERGY_ELEC.value]) + + + def get_module(self, module: dict) -> Module: """Return module.""" @@ -119,10 +144,7 @@ def update_topology(self, raw_data: RawData) -> None: for room in self.rooms.keys() - {m["id"] for m in raw_rooms}: self.rooms.pop(room) - self.schedules = { - s["id"]: Schedule(home=self, raw_data=s) - for s in raw_data.get(SCHEDULES, []) - } + self._handle_schedules(raw_data.get(SCHEDULES, [])) async def update(self, raw_data: RawData) -> None: """Update home with the latest data.""" @@ -156,18 +178,28 @@ async def update(self, raw_data: RawData) -> None: ], ) - def get_selected_schedule(self) -> Schedule | None: + def get_selected_schedule(self, type :str = None) -> Schedule | None: """Return selected schedule for given home.""" + if type is None: + type = SCHEDULE_TYPE_THERM + + schedules = self.all_schedules.get(type, {}) return next( - (schedule for schedule in self.schedules.values() if schedule.selected), + (schedule for schedule in schedules.values() if schedule.selected), None, ) + def get_selected_temperature_schedule(self) -> ThermSchedule | None: + return self.get_selected_schedule(type=SCHEDULE_TYPE_THERM) + def is_valid_schedule(self, schedule_id: str) -> bool: """Check if valid schedule.""" + for schedules in self.all_schedules.values(): + if schedule_id in schedules: + return True - return schedule_id in self.schedules + return False def has_otm(self) -> bool: """Check if any room has an OTM device.""" @@ -177,14 +209,14 @@ def has_otm(self) -> bool: def get_hg_temp(self) -> float | None: """Return frost guard temperature value for given home.""" - if (schedule := self.get_selected_schedule()) is None: + if (schedule := self.get_selected_temperature_schedule()) is None: return None return schedule.hg_temp def get_away_temp(self) -> float | None: """Return configured away temperature value for given home.""" - if (schedule := self.get_selected_schedule()) is None: + if (schedule := self.get_selected_temperature_schedule()) is None: return None return schedule.away_temp @@ -276,7 +308,7 @@ async def async_set_schedule_temperatures( ) -> None: """Set the scheduled room temperature for the given schedule ID.""" - selected_schedule = self.get_selected_schedule() + selected_schedule = self.get_selected_temperature_schedule() if selected_schedule is None: raise NoSchedule("Could not determine selected schedule.") diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index b4f1d81a..08b76bfc 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -2,14 +2,13 @@ from __future__ import annotations from datetime import datetime, timezone -from enum import Enum import logging from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError -from pyatmo.const import GETMEASURE_ENDPOINT, RawData -from pyatmo.exceptions import ApiError +from pyatmo.const import GETMEASURE_ENDPOINT, RawData, MeasureInterval +from pyatmo.exceptions import ApiError, InvalidHistoryFromAPI from pyatmo.modules.base_class import EntityBase, NetatmoBase, Place from pyatmo.modules.device_types import DEVICE_CATEGORY_MAP, DeviceCategory, DeviceType @@ -583,34 +582,6 @@ async def async_monitoring_off(self) -> bool: return await self.async_set_monitoring_state("off") -class MeasureInterval(Enum): - """Measure interval.""" - - HALF_HOUR = "30min" - HOUR = "1hour" - THREE_HOURS = "3hours" - DAY = "1day" - WEEK = "1week" - MONTH = "1month" - - -class MeasureType(Enum): - """Measure type.""" - - BOILERON = "boileron" - BOILEROFF = "boileroff" - SUM_BOILER_ON = "sum_boiler_on" - SUM_BOILER_OFF = "sum_boiler_off" - SUM_ENERGY_ELEC = "sum_energy_elec" - SUM_ENERGY_ELEC_BASIC = "sum_energy_elec$0" - SUM_ENERGY_ELEC_PEAK = "sum_energy_elec$1" - SUM_ENERGY_ELEC_OFF_PEAK = "sum_energy_elec$2" - SUM_ENERGY_PRICE = "sum_energy_price" - SUM_ENERGY_PRICE_BASIC = "sum_energy_price$0" - SUM_ENERGY_PRICE_PEAK = "sum_energy_price$1" - SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_price$2" - - class HistoryMixin(EntityBase): """Mixin for history data.""" @@ -634,41 +605,86 @@ async def async_update_measures( if start_time is None: start_time = end_time - days * 24 * 60 * 60 - data_point = MeasureType.SUM_ENERGY_ELEC_BASIC.value - params = { - "device_id": self.bridge, - "module_id": self.entity_id, - "scale": interval.value, - "type": data_point, - "date_begin": start_time, - "date_end": end_time, - } + data_points = self.home.energy_endpoints + raw_datas = [] - resp = await self.home.auth.async_post_api_request( - endpoint=GETMEASURE_ENDPOINT, - params=params, - ) - raw_data = await resp.json() + for data_point in data_points: - data = raw_data["body"][0] - interval_sec = int(data["step_time"]) - interval_min = interval_sec // 60 + params = { + "device_id": self.bridge, + "module_id": self.entity_id, + "scale": interval.value, + "type": data_point, + "date_begin": start_time, + "date_end": end_time, + } + + resp = await self.home.auth.async_post_api_request( + endpoint=GETMEASURE_ENDPOINT, + params=params, + ) + raw_datas.append(await resp.json()) self.historical_data = [] + self.historical_data_per_data_point = {data_point : [] for data_point in data_points} + + raw_datas = [raw_data["body"] for raw_data in raw_datas] + + data = raw_datas[0][0] + + if len(data) == 0: + raise InvalidHistoryFromAPI(f"No energy historical data from {data_points[0]}") + + + interval_sec = int(data["step_time"]) + interval_min = interval_sec // 60 self.start_time = int(data["beg_time"]) - start_time = self.start_time - for value in data["value"]: - end_time = start_time + interval_sec - self.historical_data.append( - { - "duration": interval_min, - "startTime": f"{datetime.fromtimestamp(start_time + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", - "endTime": f"{datetime.fromtimestamp(end_time, tz=timezone.utc).isoformat().split('+')[0]}Z", - "Wh": value[0], - }, - ) - start_time = end_time + + if len(raw_datas) > 1: + #check that all data are well aligned and compatible + beg_times = {} + + for rg in raw_datas[0]: + beg_times[(int(rg["beg_time"]))] = len(rg["value"]) + + for raw_data in raw_datas: + for rg in raw_data: + if int(rg["step_time"]) != interval_sec: + raise InvalidHistoryFromAPI(f"Invalid energy historical data from {data_points}, step_time mismatch") + b = int(rg["beg_time"]) + if b not in beg_times: + raise InvalidHistoryFromAPI(f"Invalid energy historical data from {data_points}, beg_time mismatch") + if beg_times[b] != len(rg["value"]): + raise InvalidHistoryFromAPI(f"Invalid energy historical data from {data_points}, value size mismatch mismatch") + + + + for i_step, step_0 in enumerate(raw_datas[0]): + start_time = int(step_0["beg_time"]) + for i_value in range(len(step_0["value"])): + end_time = start_time + interval_sec + tot_val = 0 + vals = [] + for raw_data in raw_datas: + val = int(raw_data[i_step]["value"][i_value][0]) + tot_val += val + vals.append(val) + + self.historical_data.append( + { + "duration": interval_min, + "startTime": f"{datetime.fromtimestamp(start_time + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", + "endTime": f"{datetime.fromtimestamp(end_time, tz=timezone.utc).isoformat().split('+')[0]}Z", + "Wh": tot_val, + "allWh" : vals, + "startTimeUnix": start_time, + "endTimeUnix": end_time + + }, + ) + + start_time = end_time class Module(NetatmoBase): diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index 3b90336b..a96c644e 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -5,7 +5,8 @@ import logging from typing import TYPE_CHECKING -from pyatmo.const import RawData +from pyatmo.const import RawData, SCHEDULE_TYPE_THERM, SCHEDULE_TYPE_EVENT, SCHEDULE_TYPE_ELECTRICITY, \ + SCHEDULE_TYPE_COOLING from pyatmo.modules.base_class import NetatmoBase from pyatmo.room import Room @@ -20,23 +21,85 @@ class Schedule(NetatmoBase): """Class to represent a Netatmo schedule.""" selected: bool - away_temp: float | None - hg_temp: float | None + default: bool + type: str timetable: list[TimetableEntry] def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule instance.""" super().__init__(raw_data) self.home = home + self.type = raw_data.get("type", "therm") self.selected = raw_data.get("selected", False) - self.hg_temp = raw_data.get("hg_temp") - self.away_temp = raw_data.get("away_temp") + self.default = raw_data.get("default", False) self.timetable = [ TimetableEntry(home, r) for r in raw_data.get("timetable", []) ] self.zones = [Zone(home, r) for r in raw_data.get("zones", [])] + + +@dataclass +class ThermSchedule(Schedule): + """Class to represent a Netatmo Temperature schedule.""" + + away_temp: float | None + hg_temp: float | None + + def __init__(self, home: Home, raw_data: RawData) -> None: + super().__init__(home, raw_data) + self.hg_temp = raw_data.get("hg_temp") + self.away_temp = raw_data.get("away_temp") + + + +@dataclass +class CoolingSchedule(ThermSchedule): + """Class to represent a Netatmo Cooling schedule.""" + + cooling_away_temp: float | None + hg_temp: float | None + + def __init__(self, home: Home, raw_data: RawData) -> None: + super().__init__(home, raw_data) + self.hg_temp = raw_data.get("hg_temp") + self.cooling_away_temp = self.away_temp = raw_data.get("cooling_away_temp", self.away_temp) + +@dataclass +class ElectricitySchedule(Schedule): + """Class to represent a Netatmo Energy Plan schedule.""" + + tariff: str + tariff_option: str + power_threshold: int | 6 + contract_power_unit: str #kVA or KW + + def __init__(self, home: Home, raw_data: RawData) -> None: + super().__init__(home, raw_data) + self.tariff = raw_data.get("tariff", "custom") + self.tariff_option = raw_data.get("tariff_option", None) + self.power_threshold = raw_data.get("power_threshold", 6) + self.contract_power_unit = raw_data.get("power_threshold", "kVA") + + + + +@dataclass +class EventSchedule(Schedule): + """Class to represent a Netatmo Energy Plan schedule.""" + + timetable_sunrise: list[TimetableEventEntry] + timetable_sunset: list[TimetableEventEntry] + def __init__(self, home: Home, raw_data: RawData) -> None: + super().__init__(home, raw_data) + self.timetable_sunrise = [ + TimetableEventEntry(home, r) for r in raw_data.get("timetable_sunrise", []) + ] + self.timetable_sunset = [ + TimetableEventEntry(home, r) for r in raw_data.get("timetable_sunset", []) + ] + @dataclass class TimetableEntry: """Class to represent a Netatmo schedule's timetable entry.""" @@ -49,6 +112,40 @@ def __init__(self, home: Home, raw_data: RawData) -> None: self.home = home self.zone_id = raw_data.get("zone_id", 0) self.m_offset = raw_data.get("m_offset", 0) + self.twilight_offset = raw_data.get("twilight_offset", 0) + + +@dataclass +class TimetableEventEntry: + """Class to represent a Netatmo schedule's timetable entry.""" + + zone_id: int | None + day: int | 1 + twilight_offset: int | 0 + def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize a Netatmo schedule's timetable entry instance.""" + self.home = home + self.zone_id = raw_data.get("zone_id", 0) + self.day = raw_data.get("day", 1) + self.twilight_offset = raw_data.get("twilight_offset", 0) + + + +class ModuleSchedule(NetatmoBase): + + on: bool + target_position: int + fan_speed: int + brightness: int + + def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize a Netatmo schedule's zone instance.""" + super().__init__(raw_data) + self.home = home + self.on = raw_data.get("on", None) + self.target_position = raw_data.get("target_position", None) + self.fan_speed = raw_data.get("fan_speed", None) + self.brightness = raw_data.get("brightness", None) @dataclass @@ -57,6 +154,7 @@ class Zone(NetatmoBase): type: int rooms: list[Room] + modules: list[ModuleSchedule] def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule's zone instance.""" @@ -70,3 +168,12 @@ def room_factory(home: Home, room_raw_data: RawData): return room self.rooms = [room_factory(home, r) for r in raw_data.get("rooms", [])] + self.modules = [ModuleSchedule(home, m) for m in raw_data.get("modules", [])] + + + +def schedule_factory(home: Home, raw_data: RawData) -> (Schedule, str): + type = raw_data.get("type", "custom") + cls = {SCHEDULE_TYPE_THERM: ThermSchedule, SCHEDULE_TYPE_EVENT: EventSchedule, SCHEDULE_TYPE_ELECTRICITY: ElectricitySchedule, SCHEDULE_TYPE_COOLING: CoolingSchedule}.get(type, Schedule) + return cls(home, raw_data), type + diff --git a/tests/test_home.py b/tests/test_home.py index 5b1506c4..92c32556 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -160,15 +160,21 @@ async def test_historical_data_retrieval(async_account): await async_account.async_update_measures(home_id=home_id, module_id=module_id) assert module.historical_data[0] == { "Wh": 197, + "allWh": [197], "duration": 60, "startTime": "2022-02-05T08:29:50Z", "endTime": "2022-02-05T09:29:49Z", + "endTimeUnix": 1644053389, + "startTimeUnix": 1644049789 } assert module.historical_data[-1] == { "Wh": 259, + 'allWh': [259], "duration": 60, "startTime": "2022-02-12T07:29:50Z", + "startTimeUnix": 1644650989, "endTime": "2022-02-12T08:29:49Z", + "endTimeUnix": 1644654589, } assert len(module.historical_data) == 168 From 0962f81dd4c65dbf2497c52f67895daee4386f82 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 20 Feb 2024 01:39:53 +0100 Subject: [PATCH 02/97] Corrected devices to use hstoryMixin to get energy --- src/pyatmo/modules/bticino.py | 4 ++-- src/pyatmo/modules/legrand.py | 18 +++++++++--------- src/pyatmo/modules/module.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pyatmo/modules/bticino.py b/src/pyatmo/modules/bticino.py index ebfd32d0..cfa0979f 100644 --- a/src/pyatmo/modules/bticino.py +++ b/src/pyatmo/modules/bticino.py @@ -3,7 +3,7 @@ import logging -from pyatmo.modules.module import Dimmer, Module, Shutter, Switch +from pyatmo.modules.module import Dimmer, Module, Shutter, Switch, OffloadMixin LOG = logging.getLogger(__name__) @@ -60,7 +60,7 @@ class BNTR(Module): """BTicino radiator thermostat.""" -class BNIL(Switch): +class BNIL(Switch, OffloadMixin): """BTicino itelligent light.""" diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index ebf954c2..754f3a6d 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -34,11 +34,11 @@ class NLT(FirmwareMixin, BatteryMixin, Module): """Legrand global remote control.""" -class NLP(Switch, HistoryMixin, PowerMixin, OffloadMixin, Module): +class NLP(Switch, OffloadMixin): """Legrand plug.""" -class NLPM(Switch): +class NLPM(Switch, OffloadMixin): """Legrand mobile plug.""" @@ -46,7 +46,7 @@ class NLPO(ContactorMixin, OffloadMixin, Switch): """Legrand contactor.""" -class NLPT(Switch): +class NLPT(Switch, OffloadMixin): """Legrand latching relay/teleruptor.""" @@ -78,7 +78,7 @@ class NLD(Dimmer): """Legrand Double On/Off dimmer remote.""" -class NLL(FirmwareMixin, EnergyMixin, WifiMixin, SwitchMixin, Module): +class NLL(Switch, WifiMixin): """Legrand / BTicino italian light switch with neutral.""" @@ -102,11 +102,11 @@ class NLE(FirmwareMixin, HistoryMixin, PowerMixin, EnergyMixin, Module): """Legrand / BTicino connected ecometer.""" -class NLPS(FirmwareMixin, PowerMixin, EnergyMixin, Module): +class NLPS(FirmwareMixin, HistoryMixin, PowerMixin, EnergyMixin, Module): """Legrand / BTicino smart load shedder.""" -class NLC(FirmwareMixin, SwitchMixin, HistoryMixin, PowerMixin, OffloadMixin, Module): +class NLC(Switch, OffloadMixin): """Legrand / BTicino cable outlet.""" @@ -114,7 +114,7 @@ class NLDD(FirmwareMixin, Module): """Legrand NLDD dimmer remote control.""" -class NLUP(FirmwareMixin, PowerMixin, SwitchMixin, Module): +class NLUP(Switch): """Legrand NLUP Power outlet.""" @@ -134,7 +134,7 @@ class NLUO(Dimmer): """Legrand NLUO device stub.""" -class NLLF(Fan): +class NLLF(Fan, EnergyMixin, PowerMixin, HistoryMixin): """Legrand NLLF fan/ventilation device.""" @@ -158,7 +158,7 @@ class NLTS(Module): """NLTS motion sensor.""" -class NLPD(FirmwareMixin, SwitchMixin, EnergyMixin, PowerMixin, Module): +class NLPD(Switch, OffloadMixin): """NLPD dry contact.""" diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 08b76bfc..10c2a272 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -758,7 +758,7 @@ async def update(self, raw_data: RawData) -> None: await self.async_update_camera_urls() -class Switch(FirmwareMixin, PowerMixin, SwitchMixin, Module): +class Switch(FirmwareMixin, HistoryMixin, EnergyMixin, PowerMixin, SwitchMixin, Module): """Class to represent a Netatmo switch.""" ... From 7faa1ad923a3e7d74db83461232c76dfa4000301 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 20 Feb 2024 23:58:09 +0100 Subject: [PATCH 03/97] Merged historyMixin and Energy has they are effectively doing the sum, introduces a sum energy and an helper to be used in homeassistant primarily --- src/pyatmo/modules/legrand.py | 11 +++---- src/pyatmo/modules/module.py | 62 +++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 754f3a6d..4d00907b 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -7,10 +7,9 @@ BatteryMixin, ContactorMixin, Dimmer, - EnergyMixin, Fan, FirmwareMixin, - HistoryMixin, + EnergyHistoryMixin, Module, OffloadMixin, PowerMixin, @@ -94,15 +93,15 @@ class NLLM(FirmwareMixin, RfMixin, ShutterMixin, Module): """Legrand / BTicino shutters.""" -class NLPC(FirmwareMixin, HistoryMixin, PowerMixin, EnergyMixin, Module): +class NLPC(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): """Legrand / BTicino connected energy meter.""" -class NLE(FirmwareMixin, HistoryMixin, PowerMixin, EnergyMixin, Module): +class NLE(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): """Legrand / BTicino connected ecometer.""" -class NLPS(FirmwareMixin, HistoryMixin, PowerMixin, EnergyMixin, Module): +class NLPS(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): """Legrand / BTicino smart load shedder.""" @@ -134,7 +133,7 @@ class NLUO(Dimmer): """Legrand NLUO device stub.""" -class NLLF(Fan, EnergyMixin, PowerMixin, HistoryMixin): +class NLLF(Fan, PowerMixin, EnergyHistoryMixin): """Legrand NLLF fan/ventilation device.""" diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 10c2a272..6b6e60ed 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -1,7 +1,7 @@ """Module to represent a Netatmo module.""" from __future__ import annotations -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta import logging from typing import TYPE_CHECKING, Any @@ -287,16 +287,6 @@ def __init__(self, home: Home, module: ModuleT): self.appliance_type: str | None = None -class EnergyMixin(EntityBase): - """Mixin for energy data.""" - - def __init__(self, home: Home, module: ModuleT): - """Initialize energy mixin.""" - - super().__init__(home, module) # type: ignore # mypy issue 4335 - self.sum_energy_elec: int | None = None - - class PowerMixin(EntityBase): """Mixin for power data.""" @@ -582,7 +572,7 @@ async def async_monitoring_off(self) -> bool: return await self.async_set_monitoring_state("off") -class HistoryMixin(EntityBase): +class EnergyHistoryMixin(EntityBase): """Mixin for history data.""" def __init__(self, home: Home, module: ModuleT): @@ -591,19 +581,47 @@ def __init__(self, home: Home, module: ModuleT): super().__init__(home, module) # type: ignore # mypy issue 4335 self.historical_data: list[dict[str, Any]] | None = None self.start_time: int | None = None + self.end_time: int | None = None self.interval: MeasureInterval | None = None + self.sum_energy_elec: int | None = None + + async def async_update_current_day_energy_measures(self) -> None: + end = datetime.now() + end_time = int(end.timestamp()) + + #go at the begining of the current day + start_time = datetime(end.year, end.month, end.day) + timedelta(seconds=1) + start_time = int(start_time.timestamp()) + + if self.end_time is not None: + prev_end = datetime.fromtimestamp(self.end_time) + if prev_end.day != end.day: + #we are in a "change of day" ask as a measure: reset the energy sum and that's all + self.end_time = end_time + self.start_time = start_time + self.historical_data = [] + self.sum_energy_elec = 0 + return + + await self.async_update_measures(start_time=start_time, end_time=end_time) + async def async_update_measures( self, start_time: int | None = None, + end_time: int | None = None, interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, + days: int = 7 ) -> None: """Update historical data.""" - end_time = int(datetime.now().timestamp()) + if end_time is None: + end_time = int(datetime.now().timestamp()) + if start_time is None: - start_time = end_time - days * 24 * 60 * 60 + end = datetime.fromtimestamp(end_time) + start_time = end - timedelta(days=days) + start_time = int(start_time.timestamp()) data_points = self.home.energy_endpoints raw_datas = [] @@ -626,7 +644,6 @@ async def async_update_measures( raw_datas.append(await resp.json()) self.historical_data = [] - self.historical_data_per_data_point = {data_point : [] for data_point in data_points} raw_datas = [raw_data["body"] for raw_data in raw_datas] @@ -637,8 +654,7 @@ async def async_update_measures( interval_sec = int(data["step_time"]) - interval_min = interval_sec // 60 - self.start_time = int(data["beg_time"]) + if len(raw_datas) > 1: @@ -658,10 +674,14 @@ async def async_update_measures( if beg_times[b] != len(rg["value"]): raise InvalidHistoryFromAPI(f"Invalid energy historical data from {data_points}, value size mismatch mismatch") - + self.sum_energy_elec = 0 + self.end_time = end_time + self.start_time = int(data["beg_time"]) for i_step, step_0 in enumerate(raw_datas[0]): start_time = int(step_0["beg_time"]) + interval_sec = int(step_0["step_time"]) + interval_min = interval_sec // 60 for i_value in range(len(step_0["value"])): end_time = start_time + interval_sec tot_val = 0 @@ -670,7 +690,7 @@ async def async_update_measures( val = int(raw_data[i_step]["value"][i_value][0]) tot_val += val vals.append(val) - + self.sum_energy_elec += val self.historical_data.append( { "duration": interval_min, @@ -758,7 +778,7 @@ async def update(self, raw_data: RawData) -> None: await self.async_update_camera_urls() -class Switch(FirmwareMixin, HistoryMixin, EnergyMixin, PowerMixin, SwitchMixin, Module): +class Switch(FirmwareMixin, EnergyHistoryMixin, PowerMixin, SwitchMixin, Module): """Class to represent a Netatmo switch.""" ... From 4db8c5c696c63b2be4043409fceb7a3f6c947031 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sat, 24 Feb 2024 12:45:54 +0100 Subject: [PATCH 04/97] removed the daily update --- src/pyatmo/modules/module.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 6b6e60ed..31d24dcb 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -585,27 +585,7 @@ def __init__(self, home: Home, module: ModuleT): self.interval: MeasureInterval | None = None self.sum_energy_elec: int | None = None - async def async_update_current_day_energy_measures(self) -> None: - end = datetime.now() - end_time = int(end.timestamp()) - - #go at the begining of the current day - start_time = datetime(end.year, end.month, end.day) + timedelta(seconds=1) - start_time = int(start_time.timestamp()) - - if self.end_time is not None: - prev_end = datetime.fromtimestamp(self.end_time) - if prev_end.day != end.day: - #we are in a "change of day" ask as a measure: reset the energy sum and that's all - self.end_time = end_time - self.start_time = start_time - self.historical_data = [] - self.sum_energy_elec = 0 - return - - await self.async_update_measures(start_time=start_time, end_time=end_time) - - + async def async_update_measures( self, start_time: int | None = None, From 9bfe32ada8933456c4c128596e013eb936caf7d8 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sat, 24 Feb 2024 13:02:58 +0100 Subject: [PATCH 05/97] added a reset measures method to be used directly by external libreries --- src/pyatmo/modules/module.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 31d24dcb..f393175d 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -585,6 +585,11 @@ def __init__(self, home: Home, module: ModuleT): self.interval: MeasureInterval | None = None self.sum_energy_elec: int | None = None + def reset_measures(self): + self.historical_data = [] + self.sum_energy_elec = 0 + + async def async_update_measures( self, From aa841c423df89002c2a36e5f67f37c1fc16ec7a6 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 26 Feb 2024 10:17:24 -0400 Subject: [PATCH 06/97] reset now resets timings too --- src/pyatmo/modules/module.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index f393175d..a5f0fbbb 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -586,6 +586,8 @@ def __init__(self, home: Home, module: ModuleT): self.sum_energy_elec: int | None = None def reset_measures(self): + self.start_time = None + self.end_time = None self.historical_data = [] self.sum_energy_elec = 0 From f7637851dece705ed3fae06a9df4b4784cd2b5c7 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 7 Mar 2024 01:57:41 +0100 Subject: [PATCH 07/97] Try to get a proper historical data in case of peak / off_peak energy mode, add some tests --- ...nergy_elec$0_12_34_56_00_00_a1_4c_da.json} | 0 ...energy_elec$1_98_76_54_32_10_00_00_73.json | 286 + ...energy_elec$2_98_76_54_32_10_00_00_73.json | 154 + fixtures/homesdata_multi.json | 6442 +++++++++++++++++ src/pyatmo/account.py | 2 + src/pyatmo/const.py | 10 + src/pyatmo/home.py | 45 +- src/pyatmo/modules/module.py | 202 +- src/pyatmo/schedule.py | 32 +- tests/common.py | 15 +- tests/conftest.py | 27 +- tests/test_energy.py | 72 + tests/test_home.py | 38 +- 13 files changed, 7226 insertions(+), 99 deletions(-) rename fixtures/{getmeasure_12_34_56_00_00_a1_4c_da.json => getmeasure_sum_energy_elec$0_12_34_56_00_00_a1_4c_da.json} (100%) create mode 100644 fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_73.json create mode 100644 fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_73.json create mode 100644 fixtures/homesdata_multi.json diff --git a/fixtures/getmeasure_12_34_56_00_00_a1_4c_da.json b/fixtures/getmeasure_sum_energy_elec$0_12_34_56_00_00_a1_4c_da.json similarity index 100% rename from fixtures/getmeasure_12_34_56_00_00_a1_4c_da.json rename to fixtures/getmeasure_sum_energy_elec$0_12_34_56_00_00_a1_4c_da.json diff --git a/fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_73.json b/fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_73.json new file mode 100644 index 00000000..c1d788b2 --- /dev/null +++ b/fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_73.json @@ -0,0 +1,286 @@ +{ + "body": [ + { + "beg_time": 1709421900, + "step_time": 1800, + "value": [ + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ] + ] + }, + { + "beg_time": 1709459700, + "step_time": 1800, + "value": [ + [ + 526 + ], + [ + 1194 + ], + [ + 1195 + ], + [ + 1176 + ], + [ + 1177 + ], + [ + 1164 + ], + [ + 1524 + ], + [ + 416 + ], + [ + 659 + ], + [ + 743 + ], + [ + 1033 + ], + [ + 1052 + ], + [ + 964 + ], + [ + 985 + ], + [ + 1031 + ], + [ + 924 + ], + [ + 73 + ], + [ + 156 + ], + [ + 320 + ], + [ + 268 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 768 + ], + [ + 912 + ], + [ + 889 + ], + [ + 399 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 38 + ], + [ + 38 + ], + [ + 0 + ], + [ + 218 + ], + [ + 186 + ], + [ + 250 + ], + [ + 363 + ], + [ + 479 + ], + [ + 579 + ], + [ + 680 + ], + [ + 832 + ], + [ + 872 + ], + [ + 699 + ], + [ + 37 + ], + [ + 77 + ], + [ + 248 + ], + [ + 209 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 418 + ], + [ + 690 + ], + [ + 714 + ], + [ + 327 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 173 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 91 + ], + [ + 212 + ], + [ + 199 + ], + [ + 161 + ], + [ + 291 + ], + [ + 394 + ], + [ + 617 + ], + [ + 688 + ], + [ + 526 + ], + [ + 428 + ], + [ + 0 + ] + ] + } + ], + "status": "ok", + "time_exec": 0.10495901107788086, + "time_server": 1709768260 +} \ No newline at end of file diff --git a/fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_73.json b/fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_73.json new file mode 100644 index 00000000..10f8884a --- /dev/null +++ b/fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_73.json @@ -0,0 +1,154 @@ +{ + "body": [ + { + "beg_time": 1709421900, + "step_time": 1800, + "value": [ + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ] + ] + }, + { + "beg_time": 1709459700, + "step_time": 1800, + "value": [ + [ + 728 + ], + [ + 1196 + ], + [ + 1197 + ], + [ + 1206 + ], + [ + 1167 + ], + [ + 887 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 200 + ], + [ + 1532 + ], + [ + 165 + ], + [ + 252 + ], + [ + 282 + ], + [ + 195 + ], + [ + 307 + ], + [ + 396 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 201 + ], + [ + 1308 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ] + ] + } + ], + "status": "ok", + "time_exec": 0.13054180145263672, + "time_server": 1709768401 +} \ No newline at end of file diff --git a/fixtures/homesdata_multi.json b/fixtures/homesdata_multi.json new file mode 100644 index 00000000..7a686ed0 --- /dev/null +++ b/fixtures/homesdata_multi.json @@ -0,0 +1,6442 @@ +{ + "body": { + "homes": [ + { + "id": "aaaaaaaaaaabbbbbbbbbbccc", + "name": "A BIG HOME", + "altitude": 284, + "coordinates": [ + 26.234678, + 83.234678 + ], + "country": "FR", + "timezone": "Europe/Paris", + "rooms": [ + { + "id": "3707962039", + "name": "Cuisine", + "type": "kitchen", + "module_ids": [ + "98:76:54:32:10:00:00:03", + "98:76:54:32:10:00:00:05", + "98:76:54:32:10:00:00:06", + "98:76:54:32:10:00:00:07", + "98:76:54:32:10:00:00:09", + "98:76:54:32:10:00:00:28", + "98:76:54:32:10:00:00:37", + "98:76:54:32:10:00:00:38", + "98:76:54:32:10:00:00:40", + "98:76:54:32:10:00:00:50" + ] + }, + { + "id": "596817675", + "name": "Vestibule", + "type": "lobby", + "module_ids": [ + "98:76:54:32:10:00:00:01", + "98:76:54:32:10:00:00:02", + "98:76:54:32:10:00:00:08", + "98:76:54:32:10:00:00:13", + "98:76:54:32:10:00:00:14", + "98:76:54:32:10:00:00:15", + "98:76:54:32:10:00:00:16", + "98:76:54:32:10:00:00:27", + "98:76:54:32:10:00:00:29", + "98:76:54:32:10:00:00:32", + "98:76:54:32:10:00:00:36", + "98:76:54:32:10:00:00:39" + ] + }, + { + "id": "1462100035", + "name": "Salon", + "type": "livingroom", + "module_ids": [ + "98:76:54:32:10:00:00:04", + "98:76:54:32:10:00:00:24#1", + "98:76:54:32:10:00:00:34", + "98:76:54:32:10:00:00:52", + "98:76:54:32:10:00:00:60", + "98:76:54:32:10:00:00:63", + "98:76:54:32:10:00:00:66", + "98:76:54:32:10:00:00:73", + "98:76:54:32:10:00:00:75" + ] + }, + { + "id": "3435163850", + "name": "Chambre A", + "type": "bedroom", + "module_ids": [ + "98:76:54:32:10:00:00:26", + "98:76:54:32:10:00:00:51" + ] + }, + { + "id": "737850817", + "name": "Extérieur", + "type": "outdoor", + "module_ids": [ + "98:76:54:32:10:00:00:10", + "98:76:54:32:10:00:00:41", + "98:76:54:32:10:00:00:42", + "98:76:54:32:10:00:00:53", + "98:76:54:32:10:00:00:54", + "98:76:54:32:10:00:00:55", + "98:76:54:32:10:00:00:56" + ] + }, + { + "id": "842662884", + "name": "Bibliothèque", + "type": "home_office", + "module_ids": [ + "98:76:54:32:10:00:00:11", + "98:76:54:32:10:00:00:12", + "98:76:54:32:10:00:00:24#2", + "98:76:54:32:10:00:00:25" + ] + }, + { + "id": "3194154910", + "name": "Salle à manger", + "type": "dining_room", + "module_ids": [ + "98:76:54:32:10:00:00:30", + "98:76:54:32:10:00:00:31", + "98:76:54:32:10:00:00:33", + "98:76:54:32:10:00:00:72" + ] + }, + { + "id": "2370728183", + "name": "Atelier", + "type": "custom", + "module_ids": [ + "98:76:54:32:10:00:00:18", + "98:76:54:32:10:00:00:23" + ] + }, + { + "id": "2042969726", + "name": "Chambre B", + "type": "bedroom", + "module_ids": [ + "98:76:54:32:10:00:00:19", + "98:76:54:32:10:00:00:20", + "98:76:54:32:10:00:00:46" + ] + }, + { + "id": "2754296835", + "name": "Placard Technique", + "type": "electrical_cabinet", + "module_ids": [ + "aa:aa:aa:aa:aa:aa", + "98:76:54:32:10:00:00:22", + "98:76:54:32:10:00:00:48", + "98:76:54:32:10:00:00:59", + "98:76:54:32:10:00:00:67", + "98:76:54:32:10:00:00:68", + "98:76:54:32:10:00:00:69", + "98:76:54:32:10:00:00:77", + "98:76:54:32:10:00:00:78", + "98:76:54:32:10:00:00:79" + ] + }, + { + "id": "1662974901", + "name": "Dressing", + "type": "custom", + "module_ids": [ + "98:76:54:32:10:00:00:21", + "98:76:54:32:10:00:00:35" + ] + }, + { + "id": "873035982", + "name": "Chambre C", + "type": "bedroom", + "module_ids": [ + "98:76:54:32:10:00:00:17", + "98:76:54:32:10:00:00:43", + "98:76:54:32:10:00:00:76" + ] + }, + { + "id": "3795659199", + "name": "Chambre D", + "type": "bedroom", + "module_ids": [ + "98:76:54:32:10:00:00:57" + ] + }, + { + "id": "2102454491", + "name": "Buanderie", + "type": "custom", + "module_ids": [ + "98:76:54:32:10:00:00:45" + ] + }, + { + "id": "93888250", + "name": "Salle de bains Des Enfants", + "type": "bathroom", + "module_ids": [ + "98:76:54:32:10:00:00:44" + ] + }, + { + "id": "3497055021", + "name": "Chambre Brice", + "type": "bedroom", + "module_ids": [ + "98:76:54:32:10:00:00:47", + "98:76:54:32:10:00:00:74" + ] + }, + { + "id": "2061006239", + "name": "Pool house", + "type": "custom", + "module_ids": [ + "98:76:54:32:10:00:00:58", + "98:76:54:32:10:00:00:61", + "98:76:54:32:10:00:00:62", + "98:76:54:32:10:00:00:64", + "98:76:54:32:10:00:00:65", + "98:76:54:32:10:00:00:71" + ] + }, + { + "id": "927970817", + "name": "Salle de bains Des Parents", + "type": "bathroom", + "module_ids": [ + "98:76:54:32:10:00:00:49" + ] + }, + { + "id": "1641945290", + "name": "Salle de bains D’Arthur", + "type": "bathroom", + "module_ids": [ + "98:76:54:32:10:00:00:70" + ] + } + ], + "modules": [ + { + "id": "aa:aa:aa:aa:aa:aa", + "type": "NLG", + "name": "Legrand Gateway", + "setup_date": 1572624665, + "room_id": "2754296835", + "modules_bridged": [ + "98:76:54:32:10:00:00:01", + "98:76:54:32:10:00:00:02", + "98:76:54:32:10:00:00:03", + "98:76:54:32:10:00:00:04", + "98:76:54:32:10:00:00:05", + "98:76:54:32:10:00:00:06", + "98:76:54:32:10:00:00:07", + "98:76:54:32:10:00:00:08", + "98:76:54:32:10:00:00:09", + "98:76:54:32:10:00:00:10", + "98:76:54:32:10:00:00:11", + "98:76:54:32:10:00:00:12", + "98:76:54:32:10:00:00:13", + "98:76:54:32:10:00:00:14", + "98:76:54:32:10:00:00:15", + "98:76:54:32:10:00:00:16", + "98:76:54:32:10:00:00:17", + "98:76:54:32:10:00:00:18", + "98:76:54:32:10:00:00:19", + "98:76:54:32:10:00:00:20", + "98:76:54:32:10:00:00:21", + "98:76:54:32:10:00:00:22", + "98:76:54:32:10:00:00:23", + "98:76:54:32:10:00:00:24", + "98:76:54:32:10:00:00:24#1", + "98:76:54:32:10:00:00:24#2", + "98:76:54:32:10:00:00:25", + "98:76:54:32:10:00:00:26", + "98:76:54:32:10:00:00:27", + "98:76:54:32:10:00:00:28", + "98:76:54:32:10:00:00:29", + "98:76:54:32:10:00:00:30", + "98:76:54:32:10:00:00:31", + "98:76:54:32:10:00:00:32", + "98:76:54:32:10:00:00:33", + "98:76:54:32:10:00:00:34", + "98:76:54:32:10:00:00:35", + "98:76:54:32:10:00:00:36", + "98:76:54:32:10:00:00:37", + "98:76:54:32:10:00:00:38", + "98:76:54:32:10:00:00:39", + "98:76:54:32:10:00:00:40", + "98:76:54:32:10:00:00:41", + "98:76:54:32:10:00:00:42", + "98:76:54:32:10:00:00:43", + "98:76:54:32:10:00:00:44", + "98:76:54:32:10:00:00:45", + "98:76:54:32:10:00:00:46", + "98:76:54:32:10:00:00:47", + "98:76:54:32:10:00:00:48", + "98:76:54:32:10:00:00:49", + "98:76:54:32:10:00:00:50", + "98:76:54:32:10:00:00:51", + "98:76:54:32:10:00:00:52", + "98:76:54:32:10:00:00:53", + "98:76:54:32:10:00:00:54", + "98:76:54:32:10:00:00:55", + "98:76:54:32:10:00:00:03#1", + "98:76:54:32:10:00:00:03#2", + "98:76:54:32:10:00:00:07#1", + "98:76:54:32:10:00:00:07#2", + "98:76:54:32:10:00:00:08#1", + "98:76:54:32:10:00:00:08#2", + "98:76:54:32:10:00:00:28#1", + "98:76:54:32:10:00:00:28#2", + "98:76:54:32:10:00:00:29#1", + "98:76:54:32:10:00:00:29#2", + "98:76:54:32:10:00:00:30#1", + "98:76:54:32:10:00:00:30#2", + "98:76:54:32:10:00:00:50#1", + "98:76:54:32:10:00:00:50#2", + "98:76:54:32:10:00:00:56", + "98:76:54:32:10:00:00:57", + "98:76:54:32:10:00:00:58", + "98:76:54:32:10:00:00:59", + "98:76:54:32:10:00:00:60", + "98:76:54:32:10:00:00:61", + "98:76:54:32:10:00:00:62", + "98:76:54:32:10:00:00:63", + "98:76:54:32:10:00:00:63#1", + "98:76:54:32:10:00:00:63#2", + "98:76:54:32:10:00:00:64", + "98:76:54:32:10:00:00:64#1", + "98:76:54:32:10:00:00:64#2", + "98:76:54:32:10:00:00:65", + "98:76:54:32:10:00:00:65#1", + "98:76:54:32:10:00:00:65#2", + "98:76:54:32:10:00:00:66", + "98:76:54:32:10:00:00:66#1", + "98:76:54:32:10:00:00:66#2", + "98:76:54:32:10:00:00:67", + "98:76:54:32:10:00:00:68", + "98:76:54:32:10:00:00:69", + "98:76:54:32:10:00:00:70", + "98:76:54:32:10:00:00:71", + "98:76:54:32:10:00:00:72", + "98:76:54:32:10:00:00:73", + "98:76:54:32:10:00:00:74", + "98:76:54:32:10:00:00:75", + "98:76:54:32:10:00:00:76", + "98:76:54:32:10:00:00:77", + "98:76:54:32:10:00:00:78", + "98:76:54:32:10:00:00:79" + ] + }, + { + "id": "98:76:54:32:10:00:00:01", + "type": "NLP", + "name": "Buffet", + "setup_date": 1572624686, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "light" + }, + { + "id": "98:76:54:32:10:00:00:02", + "type": "NLT", + "name": "Commande entrée sortie principale", + "setup_date": 1572624686, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:03", + "type": "NLD", + "name": "Boutons Cuisine Haut", + "setup_date": 1572629067, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:04", + "type": "NLV", + "name": "volet", + "setup_date": 1572798965, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:05", + "type": "NLM", + "name": "Verrière", + "setup_date": 1574591975, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:06", + "type": "NLM", + "name": "ilot", + "setup_date": 1574591975, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:07", + "type": "NLD", + "name": "Bouton Cuisine milieu", + "setup_date": 1574592863, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:08", + "type": "NLD", + "name": "Entrée Bouton Portail et dehors", + "setup_date": 1574593140, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:09", + "type": "NLT", + "name": "Bouton pour exterieur porte fenetre", + "setup_date": 1605358560, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:10", + "type": "NLM", + "name": "Olivier", + "setup_date": 1605359274, + "room_id": "737850817", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:11", + "type": "NLP", + "name": "Lampadaire", + "setup_date": 1605369621, + "room_id": "842662884", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "light" + }, + { + "id": "98:76:54:32:10:00:00:12", + "type": "NLT", + "name": "Bouton Bibliotheque", + "setup_date": 1607175439, + "room_id": "842662884", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:13", + "type": "NLF", + "name": "Couloir enfants", + "setup_date": 1612299365, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:14", + "type": "NLT", + "name": "Couloir enfants 2", + "setup_date": 1612299777, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:15", + "type": "NLT", + "name": "Couloir enfants 3", + "setup_date": 1612299852, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:16", + "type": "NLT", + "name": "Couloir enfants 1", + "setup_date": 1613565493, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:17", + "type": "NLF", + "name": "Lumière C", + "setup_date": 1617390843, + "room_id": "873035982", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:18", + "type": "NLM", + "name": "Néon", + "setup_date": 1643544945, + "room_id": "2370728183", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:19", + "type": "NLT", + "name": "Couloir A interrupteur", + "setup_date": 1643794135, + "room_id": "2042969726", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:20", + "type": "NLF", + "name": "Couloir A", + "setup_date": 1643794453, + "room_id": "2042969726", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:21", + "type": "NLPT", + "name": "Dressing", + "setup_date": 1643809582, + "room_id": "1662974901", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:22", + "type": "NLPM", + "name": "Onduleur Serveurs Mais Local Technique", + "setup_date": 1643911516, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "other" + }, + { + "id": "98:76:54:32:10:00:00:23", + "type": "NLT", + "name": "Interrupteur neon", + "setup_date": 1644008400, + "room_id": "2370728183", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:24", + "type": "NLIS", + "setup_date": 1645651106, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:24#1", + "type": "NLIS", + "name": "Niche", + "setup_date": 1645651106, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:24#2", + "type": "NLIS", + "name": "Escalier", + "setup_date": 1645651106, + "room_id": "842662884", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:25", + "type": "NLT", + "name": "Bouton Escalier", + "setup_date": 1645651356, + "room_id": "842662884", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:26", + "type": "NLPT", + "name": "Lumière Chambre E", + "setup_date": 1645657974, + "room_id": "3435163850", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:27", + "type": "NLPT", + "name": "Entrée", + "setup_date": 1645658118, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:28", + "type": "NLD", + "name": "Bouton cuisine bas", + "setup_date": 1645659939, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:29", + "type": "NLD", + "name": "Bouton double entrée porte", + "setup_date": 1645660346, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:30", + "type": "NLD", + "name": "Bouton double salle à manger", + "setup_date": 1645660684, + "room_id": "3194154910", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:31", + "type": "NLF", + "name": "Placard", + "setup_date": 1645662093, + "room_id": "3194154910", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:32", + "type": "NLT", + "name": "Bouton plafond entrée couloir", + "setup_date": 1645662629, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:33", + "type": "NLPT", + "name": "Table", + "setup_date": 1645889017, + "room_id": "3194154910", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:34", + "type": "NLPT", + "name": "Baie Vitrée", + "setup_date": 1645889069, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:35", + "type": "NLC", + "name": "Radiateur Dressing", + "setup_date": 1645894862, + "room_id": "1662974901", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:36", + "type": "NLC", + "name": "Radiateur Entrée", + "setup_date": 1645899253, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:37", + "type": "NLC", + "name": "Radiateur Cuisine Dressing", + "setup_date": 1645902157, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:38", + "type": "NLC", + "name": "Radiateur Cuisine Entrée", + "setup_date": 1645902199, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:39", + "type": "NLT", + "name": "Commande sans fil couloir enfant pour entrée", + "setup_date": 1646074736, + "room_id": "596817675", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:40", + "type": "NLPT", + "name": "Couloir Parents", + "setup_date": 1646568523, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:41", + "type": "NLPT", + "name": "Jardin", + "setup_date": 1646568567, + "room_id": "737850817", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:42", + "type": "NLPT", + "name": "Facade", + "setup_date": 1646568594, + "room_id": "737850817", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:43", + "type": "NLC", + "name": "Radiateur FF", + "setup_date": 1646581781, + "room_id": "873035982", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:44", + "type": "NLC", + "name": "Radiateur Salle De Bain Enfants", + "setup_date": 1646828219, + "room_id": "93888250", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:45", + "type": "NLC", + "name": "Radiateur Buanderie", + "setup_date": 1646828251, + "room_id": "2102454491", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:46", + "type": "NLC", + "name": "Radiateur A", + "setup_date": 1646828278, + "room_id": "2042969726", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:47", + "type": "NLC", + "name": "Radiateur B", + "setup_date": 1646828308, + "room_id": "3497055021", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:48", + "type": "NLM", + "name": "Cam Porte", + "setup_date": 1653579677, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:49", + "type": "NLC", + "name": "Radiateur Sèche Serviette E", + "setup_date": 1667205824, + "room_id": "927970817", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:50", + "type": "NLAO", + "name": "Porte vitree sans pile", + "setup_date": 1668001531, + "room_id": "3707962039", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:51", + "type": "NLC", + "name": "Radiateur E", + "setup_date": 1668339519, + "room_id": "3435163850", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:52", + "type": "NLM", + "name": "Meurtrières", + "setup_date": 1671560972, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:53", + "type": "NLPT", + "name": "Parking", + "setup_date": 1672948768, + "room_id": "737850817", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:54", + "type": "NLM", + "name": "Barbecue", + "setup_date": 1672948768, + "room_id": "737850817", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:55", + "type": "NLAO", + "name": "Bouton sans pile cuisine ete", + "setup_date": 1672949735, + "room_id": "737850817", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:03#1", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:03#2", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:07#1", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:07#2", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:08#1", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:08#2", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:28#1", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:28#2", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:29#1", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:29#2", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:30#1", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:30#2", + "type": "NLD", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:50#1", + "type": "NLAO", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:50#2", + "type": "NLAO", + "setup_date": 1673873611, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:56", + "type": "NLM", + "name": "Piscine", + "setup_date": 1691244631, + "room_id": "737850817", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:57", + "type": "NLC", + "name": "Radiateur DD", + "setup_date": 1692093769, + "room_id": "3795659199", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:58", + "type": "NLM", + "name": "Salle de bain", + "setup_date": 1692094781, + "room_id": "2061006239", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:59", + "type": "NLP", + "name": "Prise Pool House Relais Zigbee", + "setup_date": 1692095384, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "router" + }, + { + "id": "98:76:54:32:10:00:00:60", + "type": "NLPT", + "name": "Plafonnier", + "setup_date": 1692095856, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:61", + "type": "NLPT", + "name": "Terrasse Piscine", + "setup_date": 1693848343, + "room_id": "2061006239", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:62", + "type": "NLPT", + "name": "Lumière", + "setup_date": 1693848357, + "room_id": "2061006239", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:63", + "type": "NLD", + "name": "Salon Commande Double Haut", + "setup_date": 1694336106, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:63#1", + "type": "NLD", + "setup_date": 1694336106, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:63#2", + "type": "NLD", + "setup_date": 1694336106, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:64", + "type": "NLD", + "name": "Commande Pool House Gauche", + "setup_date": 1694336110, + "room_id": "2061006239", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:64#1", + "type": "NLD", + "setup_date": 1694336110, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:64#2", + "type": "NLD", + "setup_date": 1694336110, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:65", + "type": "NLD", + "name": "Commande Pool House Droite", + "setup_date": 1694336143, + "room_id": "2061006239", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:65#1", + "type": "NLD", + "setup_date": 1694336143, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:65#2", + "type": "NLD", + "setup_date": 1694336143, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:66", + "type": "NLD", + "name": "Commande Double Bas", + "setup_date": 1698577707, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:66#1", + "type": "NLD", + "setup_date": 1698577707, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:66#2", + "type": "NLD", + "setup_date": 1698577707, + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:67", + "type": "NLPO", + "name": "Cumulus Parents", + "setup_date": 1699128857, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "water_heater" + }, + { + "id": "98:76:54:32:10:00:00:68", + "type": "NLPO", + "name": "Cumulus DD", + "setup_date": 1699132798, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "water_heater" + }, + { + "id": "98:76:54:32:10:00:00:69", + "type": "NLPO", + "name": "Filtration Piscine", + "setup_date": 1699136266, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "other" + }, + { + "id": "98:76:54:32:10:00:00:70", + "type": "NLC", + "name": "Radiateur Sèche Serviette A", + "setup_date": 1702133314, + "room_id": "1641945290", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator" + }, + { + "id": "98:76:54:32:10:00:00:71", + "type": "NLPO", + "name": "Cumulus Pool House", + "setup_date": 1702135239, + "room_id": "2061006239", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "water_heater" + }, + { + "id": "98:76:54:32:10:00:00:72", + "type": "NLC", + "name": "Radiateur Sol Salle À Manger", + "setup_date": 1707332251, + "room_id": "3194154910", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator_without_pilot_wire" + }, + { + "id": "98:76:54:32:10:00:00:73", + "type": "NLC", + "name": "Radiateur Sol Salon", + "setup_date": 1707332251, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "radiator_without_pilot_wire" + }, + { + "id": "98:76:54:32:10:00:00:74", + "type": "NLP", + "name": "Setup PC B", + "setup_date": 1707332620, + "room_id": "3497055021", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "router" + }, + { + "id": "98:76:54:32:10:00:00:75", + "type": "NLPM", + "name": "Setup TV Apple TV Switch Salon", + "setup_date": 1707333771, + "room_id": "1462100035", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "multimedia" + }, + { + "id": "98:76:54:32:10:00:00:76", + "type": "NLP", + "name": "Mesure PC Switch C", + "setup_date": 1707335636, + "room_id": "873035982", + "bridge": "aa:aa:aa:aa:aa:aa", + "appliance_type": "router" + }, + { + "id": "98:76:54:32:10:00:00:77", + "type": "NLPC", + "name": "Compteur Plaque Sèche-Linge PC Buanderie Plan Cuisine", + "setup_date": 1707339526, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:78", + "type": "NLPC", + "name": "Compteur Congel Micro-onde Frigo PC WC", + "setup_date": 1708185348, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa" + }, + { + "id": "98:76:54:32:10:00:00:79", + "type": "NLPC", + "name": "Compteur Lave-linge Four Lave Lave-Vaisselle PC TV Chambre ", + "setup_date": 1708185369, + "room_id": "2754296835", + "bridge": "aa:aa:aa:aa:aa:aa" + } + ], + "temperature_control_mode": "heating", + "therm_mode": "schedule", + "therm_setpoint_default_duration": 180, + "schedules": [ + { + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 1, + "m_offset": 510 + }, + { + "zone_id": 5, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1200 + }, + { + "zone_id": 2, + "m_offset": 1260 + }, + { + "zone_id": 1, + "m_offset": 1380 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 1, + "m_offset": 1950 + }, + { + "zone_id": 5, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2640 + }, + { + "zone_id": 2, + "m_offset": 2700 + }, + { + "zone_id": 1, + "m_offset": 2820 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 1, + "m_offset": 3390 + }, + { + "zone_id": 5, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4080 + }, + { + "zone_id": 2, + "m_offset": 4140 + }, + { + "zone_id": 1, + "m_offset": 4260 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 1, + "m_offset": 4830 + }, + { + "zone_id": 5, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5520 + }, + { + "zone_id": 2, + "m_offset": 5580 + }, + { + "zone_id": 1, + "m_offset": 5700 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 1, + "m_offset": 6270 + }, + { + "zone_id": 5, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 6960 + }, + { + "zone_id": 2, + "m_offset": 7020 + }, + { + "zone_id": 1, + "m_offset": 7140 + }, + { + "zone_id": 0, + "m_offset": 7560 + }, + { + "zone_id": 1, + "m_offset": 7710 + }, + { + "zone_id": 5, + "m_offset": 8160 + }, + { + "zone_id": 1, + "m_offset": 8400 + }, + { + "zone_id": 2, + "m_offset": 8460 + }, + { + "zone_id": 1, + "m_offset": 8580 + }, + { + "zone_id": 0, + "m_offset": 9000 + }, + { + "zone_id": 1, + "m_offset": 9150 + }, + { + "zone_id": 5, + "m_offset": 9600 + }, + { + "zone_id": 1, + "m_offset": 9840 + }, + { + "zone_id": 2, + "m_offset": 9900 + }, + { + "zone_id": 1, + "m_offset": 10020 + } + ], + "zones": [ + { + "name": "Comfort", + "id": 0, + "type": 0, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Comfort+", + "id": 3, + "type": 8, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Éco", + "id": 4, + "type": 5, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "away" + } + ] + }, + { + "name": "Nuit", + "id": 1, + "type": 1, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "away" + } + ] + }, + { + "name": "Confort sauf chambres parents", + "id": 5, + "type": 4, + "rooms_temp": [ + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Chambre parents only", + "id": 2, + "type": 4, + "rooms_temp": [ + { + "room_id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + } + ], + "name": "Planning Hiver", + "default": false, + "away_temp": 12, + "hg_temp": 7, + "id": "61fa621cdd99943657260882", + "type": "therm", + "selected": true + }, + { + "timetable": [ + { + "zone_id": 2, + "m_offset": 0 + } + ], + "zones": [ + { + "name": "Comfort", + "id": 0, + "type": 0, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Comfort+", + "id": 3, + "type": 8, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Éco", + "id": 4, + "type": 5, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "away" + } + ] + }, + { + "name": "Nuit", + "id": 1, + "type": 1, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + }, + { + "name": "Off", + "id": 2, + "type": 4, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + } + ], + "name": "Planning ete", + "default": false, + "away_temp": 12, + "hg_temp": 7, + "id": "625a76ceec2cce72075ac55a", + "type": "therm" + }, + { + "timetable": [ + { + "zone_id": 0, + "m_offset": 0 + } + ], + "zones": [ + { + "name": "Comfort", + "id": 0, + "type": 0, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Night", + "id": 1, + "type": 1, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "away" + } + ] + }, + { + "name": "Comfort+", + "id": 3, + "type": 8, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Eco", + "id": 4, + "type": 5, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "away" + } + ] + } + ], + "name": "Full", + "default": false, + "away_temp": 12, + "hg_temp": 7, + "id": "63a44ba0735ffc27410f2331", + "type": "therm" + }, + { + "timetable": [ + { + "zone_id": 4, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 300 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 855 + }, + { + "zone_id": 4, + "m_offset": 1140 + }, + { + "zone_id": 0, + "m_offset": 1740 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2295 + }, + { + "zone_id": 4, + "m_offset": 2580 + }, + { + "zone_id": 0, + "m_offset": 3180 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3735 + }, + { + "zone_id": 4, + "m_offset": 4020 + }, + { + "zone_id": 0, + "m_offset": 4620 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5175 + }, + { + "zone_id": 4, + "m_offset": 5460 + }, + { + "zone_id": 0, + "m_offset": 6060 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6615 + }, + { + "zone_id": 4, + "m_offset": 6900 + }, + { + "zone_id": 0, + "m_offset": 7500 + }, + { + "zone_id": 4, + "m_offset": 7620 + }, + { + "zone_id": 0, + "m_offset": 8055 + }, + { + "zone_id": 4, + "m_offset": 8340 + }, + { + "zone_id": 0, + "m_offset": 8940 + }, + { + "zone_id": 4, + "m_offset": 9060 + }, + { + "zone_id": 0, + "m_offset": 9495 + }, + { + "zone_id": 4, + "m_offset": 9780 + } + ], + "zones": [ + { + "name": "Comfort", + "id": 0, + "type": 0, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Comfort+", + "id": 3, + "type": 8, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Éco", + "id": 4, + "type": 5, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "away" + } + ] + }, + { + "name": "Nuit", + "id": 1, + "type": 1, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + } + ], + "name": "Planning Hiver frileux", + "default": false, + "away_temp": 12, + "hg_temp": 7, + "id": "63c5b63b766611525b0b1e4d", + "type": "therm" + }, + { + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 300 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 1020 + }, + { + "zone_id": 1, + "m_offset": 1140 + }, + { + "zone_id": 0, + "m_offset": 1740 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2460 + }, + { + "zone_id": 1, + "m_offset": 2580 + }, + { + "zone_id": 0, + "m_offset": 3180 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3900 + }, + { + "zone_id": 1, + "m_offset": 4020 + }, + { + "zone_id": 0, + "m_offset": 4620 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5340 + }, + { + "zone_id": 1, + "m_offset": 5460 + }, + { + "zone_id": 0, + "m_offset": 6060 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6780 + }, + { + "zone_id": 1, + "m_offset": 6900 + }, + { + "zone_id": 0, + "m_offset": 7560 + }, + { + "zone_id": 4, + "m_offset": 7680 + }, + { + "zone_id": 0, + "m_offset": 8220 + }, + { + "zone_id": 1, + "m_offset": 8340 + }, + { + "zone_id": 0, + "m_offset": 9000 + }, + { + "zone_id": 4, + "m_offset": 9120 + }, + { + "zone_id": 0, + "m_offset": 9660 + }, + { + "zone_id": 1, + "m_offset": 9780 + } + ], + "zones": [ + { + "name": "Comfort+", + "id": 3, + "type": 8, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Nuit", + "id": 1, + "type": 1, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + }, + { + "name": "Confort", + "id": 0, + "type": 0, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + }, + { + "name": "Éco", + "id": 4, + "type": 5, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + } + ], + "name": "Chambres et SDB", + "default": false, + "away_temp": 12, + "hg_temp": 7, + "id": "6413ac548decf9f28b0efc4e", + "type": "therm" + }, + { + "timetable": [ + { + "zone_id": 5, + "m_offset": 0 + }, + { + "zone_id": 2, + "m_offset": 360 + }, + { + "zone_id": 5, + "m_offset": 450 + }, + { + "zone_id": 2, + "m_offset": 1020 + }, + { + "zone_id": 5, + "m_offset": 1080 + }, + { + "zone_id": 2, + "m_offset": 1800 + }, + { + "zone_id": 5, + "m_offset": 1890 + }, + { + "zone_id": 2, + "m_offset": 2460 + }, + { + "zone_id": 5, + "m_offset": 2520 + }, + { + "zone_id": 2, + "m_offset": 3240 + }, + { + "zone_id": 5, + "m_offset": 3330 + }, + { + "zone_id": 2, + "m_offset": 3900 + }, + { + "zone_id": 5, + "m_offset": 3960 + }, + { + "zone_id": 2, + "m_offset": 4680 + }, + { + "zone_id": 5, + "m_offset": 4770 + }, + { + "zone_id": 2, + "m_offset": 5340 + }, + { + "zone_id": 5, + "m_offset": 5400 + }, + { + "zone_id": 2, + "m_offset": 6120 + }, + { + "zone_id": 5, + "m_offset": 6210 + }, + { + "zone_id": 2, + "m_offset": 6780 + }, + { + "zone_id": 5, + "m_offset": 6840 + }, + { + "zone_id": 2, + "m_offset": 7680 + }, + { + "zone_id": 5, + "m_offset": 7740 + }, + { + "zone_id": 2, + "m_offset": 8220 + }, + { + "zone_id": 5, + "m_offset": 8280 + }, + { + "zone_id": 2, + "m_offset": 9120 + }, + { + "zone_id": 5, + "m_offset": 9180 + }, + { + "zone_id": 2, + "m_offset": 9660 + }, + { + "zone_id": 5, + "m_offset": 9720 + } + ], + "zones": [ + { + "name": "Comfort", + "id": 0, + "type": 0, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Night", + "id": 1, + "type": 1, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "away" + } + ] + }, + { + "name": "Comfort+", + "id": 3, + "type": 8, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Eco", + "id": 4, + "type": 5, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "away" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "away" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "927970817", + "therm_setpoint_fp": "away" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "away" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "away" + } + ] + }, + { + "name": "SDB seulement", + "id": 2, + "type": 4, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + }, + { + "name": "Tout Off", + "id": 5, + "type": 4, + "rooms_temp": [ + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "873035982", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + }, + { + "name": "Chambres Confort and All Off", + "id": 6, + "type": 4, + "rooms_temp": [ + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + }, + { + "name": "Chambres Eco and all Off", + "id": 7, + "type": 4, + "rooms_temp": [ + { + "room_id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "1662974901", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3707962039", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "comfort" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Chambres Eco SDB Confort All OFF", + "id": 8, + "type": 4, + "rooms_temp": [ + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "away" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "away" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "away" + }, + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "away" + }, + { + "id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "873035982", + "therm_setpoint_fp": "away" + }, + { + "id": "93888250", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "927970817", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + }, + { + "name": "Chambres & SDB Confort All OFF", + "id": 9, + "type": 4, + "rooms_temp": [ + { + "room_id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "room_id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ], + "modules": [], + "rooms": [ + { + "id": "1662974901", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "2042969726", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3435163850", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3497055021", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3707962039", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "3795659199", + "therm_setpoint_fp": "comfort" + }, + { + "id": "596817675", + "therm_setpoint_fp": "frost_guard" + }, + { + "id": "873035982", + "therm_setpoint_fp": "comfort" + }, + { + "id": "93888250", + "therm_setpoint_fp": "comfort" + }, + { + "id": "927970817", + "therm_setpoint_fp": "comfort" + }, + { + "id": "1641945290", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2102454491", + "therm_setpoint_fp": "frost_guard" + } + ] + } + ], + "name": "SDM seulement", + "default": false, + "away_temp": 12, + "hg_temp": 7, + "id": "65428fdc7349fc4e49034381", + "type": "therm" + }, + { + "timetable": [ + { + "zone_id": 1, + "m_offset": 300 + }, + { + "zone_id": 3, + "m_offset": 540 + }, + { + "zone_id": 2, + "m_offset": 960 + }, + { + "zone_id": 0, + "m_offset": 1020 + }, + { + "zone_id": 1, + "m_offset": 1740 + }, + { + "zone_id": 3, + "m_offset": 1980 + }, + { + "zone_id": 2, + "m_offset": 2400 + }, + { + "zone_id": 0, + "m_offset": 2460 + }, + { + "zone_id": 1, + "m_offset": 3180 + }, + { + "zone_id": 3, + "m_offset": 3420 + }, + { + "zone_id": 2, + "m_offset": 3840 + }, + { + "zone_id": 0, + "m_offset": 3900 + }, + { + "zone_id": 1, + "m_offset": 4620 + }, + { + "zone_id": 3, + "m_offset": 4860 + }, + { + "zone_id": 2, + "m_offset": 5280 + }, + { + "zone_id": 0, + "m_offset": 5340 + }, + { + "zone_id": 1, + "m_offset": 6060 + }, + { + "zone_id": 3, + "m_offset": 6300 + }, + { + "zone_id": 2, + "m_offset": 6720 + }, + { + "zone_id": 0, + "m_offset": 6780 + }, + { + "zone_id": 1, + "m_offset": 7500 + }, + { + "zone_id": 3, + "m_offset": 7740 + }, + { + "zone_id": 2, + "m_offset": 8160 + }, + { + "zone_id": 0, + "m_offset": 8220 + }, + { + "zone_id": 1, + "m_offset": 8940 + }, + { + "zone_id": 3, + "m_offset": 9180 + }, + { + "zone_id": 2, + "m_offset": 9600 + }, + { + "zone_id": 0, + "m_offset": 9660 + } + ], + "zones": [ + { + "id": 0, + "modules": [ + { + "id": "98:76:54:32:10:00:00:67", + "bridge": "aa:aa:aa:aa:aa:aa", + "on": false + } + ] + }, + { + "id": 1, + "modules": [ + { + "id": "98:76:54:32:10:00:00:67", + "bridge": "aa:aa:aa:aa:aa:aa", + "on": true + } + ] + }, + { + "id": 2, + "modules": [ + { + "id": "98:76:54:32:10:00:00:67", + "bridge": "aa:aa:aa:aa:aa:aa", + "on": true + } + ] + }, + { + "id": 3, + "modules": [ + { + "id": "98:76:54:32:10:00:00:67", + "bridge": "aa:aa:aa:aa:aa:aa", + "on": false + } + ] + } + ], + "name": "Planning d'actions", + "default": false, + "timetable_sunrise": [], + "timetable_sunset": [], + "id": "64fa4a1266404bbb130f4b71", + "type": "event", + "selected": true + }, + { + "timetable": [], + "zones": [], + "name": "Hiver", + "default": false, + "timetable_sunrise": [], + "timetable_sunset": [], + "id": "65c3f82f61e0a3ec5401640f", + "type": "event" + }, + { + "timetable": [ + { + "zone_id": 0, + "m_offset": 0 + }, + { + "zone_id": 1, + "m_offset": 130 + }, + { + "zone_id": 0, + "m_offset": 430 + }, + { + "zone_id": 1, + "m_offset": 850 + }, + { + "zone_id": 0, + "m_offset": 1030 + }, + { + "zone_id": 1, + "m_offset": 1570 + }, + { + "zone_id": 0, + "m_offset": 1870 + }, + { + "zone_id": 1, + "m_offset": 2290 + }, + { + "zone_id": 0, + "m_offset": 2470 + }, + { + "zone_id": 1, + "m_offset": 3010 + }, + { + "zone_id": 0, + "m_offset": 3310 + }, + { + "zone_id": 1, + "m_offset": 3730 + }, + { + "zone_id": 0, + "m_offset": 3910 + }, + { + "zone_id": 1, + "m_offset": 4450 + }, + { + "zone_id": 0, + "m_offset": 4750 + }, + { + "zone_id": 1, + "m_offset": 5170 + }, + { + "zone_id": 0, + "m_offset": 5350 + }, + { + "zone_id": 1, + "m_offset": 5890 + }, + { + "zone_id": 0, + "m_offset": 6190 + }, + { + "zone_id": 1, + "m_offset": 6610 + }, + { + "zone_id": 0, + "m_offset": 6790 + }, + { + "zone_id": 1, + "m_offset": 7330 + }, + { + "zone_id": 0, + "m_offset": 7630 + }, + { + "zone_id": 1, + "m_offset": 8050 + }, + { + "zone_id": 0, + "m_offset": 8230 + }, + { + "zone_id": 1, + "m_offset": 8770 + }, + { + "zone_id": 0, + "m_offset": 9070 + }, + { + "zone_id": 1, + "m_offset": 9490 + }, + { + "zone_id": 0, + "m_offset": 9670 + } + ], + "zones": [ + { + "id": 0, + "price": 0.27, + "price_type": "peak" + }, + { + "id": 1, + "price": 0.2068, + "price_type": "off_peak" + } + ], + "name": "electricity", + "default": false, + "tariff": "edf_tarif_bleu", + "tariff_option": "peak_and_off_peak", + "power_threshold": 36, + "contract_power_unit": "kW", + "id": "60ce3d057a24d640444c4f1c", + "type": "electricity", + "selected": true + } + ] + }, + { + "id": "eeeeeeeeeffffffffffaaaaa", + "name": "A SECOND HOME", + "altitude": 200, + "coordinates": [ + 34.345576, + 89.667112 + ], + "country": "FR", + "timezone": "Europe/Paris", + "rooms": [ + { + "id": "1468717414", + "name": "Cave", + "type": "garage", + "module_ids": [ + "12:34:56:78:90:00:00:09", + "12:34:56:78:90:00:00:20", + "12:34:56:78:90:00:00:33" + ] + }, + { + "id": "738709350", + "name": "Salon", + "type": "livingroom", + "module_ids": [ + "bb:bb:bb:bb:bb:bb", + "12:34:56:78:90:00:00:01", + "12:34:56:78:90:00:00:02", + "12:34:56:78:90:00:00:03", + "12:34:56:78:90:00:00:04", + "12:34:56:78:90:00:00:05", + "12:34:56:78:90:00:00:06", + "12:34:56:78:90:00:00:07", + "12:34:56:78:90:00:00:08", + "12:34:56:78:90:00:00:10", + "12:34:56:78:90:00:00:23", + "12:34:56:78:90:00:00:24", + "12:34:56:78:90:00:00:29", + "12:34:56:78:90:00:00:30" + ] + }, + { + "id": "3098890768", + "name": "Entrée", + "type": "lobby", + "module_ids": [ + "12:34:56:78:90:00:00:11" + ] + }, + { + "id": "70754041", + "name": "Salle de bains", + "type": "bathroom", + "module_ids": [ + "12:34:56:78:90:00:00:12", + "12:34:56:78:90:00:00:26" + ] + }, + { + "id": "1761556353", + "name": "Nouveaux toilettes", + "type": "toilets", + "module_ids": [ + "12:34:56:78:90:00:00:13" + ] + }, + { + "id": "968846272", + "name": "Anciens Toilettes", + "type": "toilets", + "module_ids": [ + "12:34:56:78:90:00:00:14" + ] + }, + { + "id": "4008422910", + "name": "Palier", + "type": "corridor", + "module_ids": [ + "12:34:56:78:90:00:00:15", + "12:34:56:78:90:00:00:18", + "12:34:56:78:90:00:00:31" + ] + }, + { + "id": "2640531479", + "name": "Chambre parents", + "type": "bedroom", + "module_ids": [ + "12:34:56:78:90:00:00:16", + "12:34:56:78:90:00:00:27" + ] + }, + { + "id": "2667868658", + "name": "Chambre Older", + "type": "bedroom", + "module_ids": [ + "12:34:56:78:90:00:00:17", + "12:34:56:78:90:00:00:22" + ] + }, + { + "id": "3783425301", + "name": "Chambre Enfants", + "type": "bedroom", + "module_ids": [ + "12:34:56:78:90:00:00:19", + "12:34:56:78:90:00:00:28" + ] + }, + { + "id": "68319658", + "name": "Chambre Old", + "type": "bedroom", + "module_ids": [ + "12:34:56:78:90:00:00:25" + ] + }, + { + "id": "1156588698", + "name": "Jardin", + "type": "outdoor", + "module_ids": [ + "12:34:56:78:90:00:00:21" + ] + }, + { + "id": "1229033409", + "name": "Grenier", + "type": "custom", + "module_ids": [ + "12:34:56:78:90:00:00:32", + "12:34:56:78:90:00:00:34" + ] + } + ], + "modules": [ + { + "id": "bb:bb:bb:bb:bb:bb", + "type": "NLG", + "name": "Legrand Gateway", + "setup_date": 1692558574, + "room_id": "738709350", + "modules_bridged": [ + "12:34:56:78:90:00:00:01", + "12:34:56:78:90:00:00:02", + "12:34:56:78:90:00:00:03", + "12:34:56:78:90:00:00:04", + "12:34:56:78:90:00:00:05", + "12:34:56:78:90:00:00:06", + "12:34:56:78:90:00:00:07", + "12:34:56:78:90:00:00:08", + "12:34:56:78:90:00:00:09", + "12:34:56:78:90:00:00:10", + "12:34:56:78:90:00:00:11", + "12:34:56:78:90:00:00:12", + "12:34:56:78:90:00:00:13", + "12:34:56:78:90:00:00:14", + "12:34:56:78:90:00:00:15", + "12:34:56:78:90:00:00:16", + "12:34:56:78:90:00:00:17", + "12:34:56:78:90:00:00:18", + "12:34:56:78:90:00:00:19", + "12:34:56:78:90:00:00:20", + "12:34:56:78:90:00:00:21", + "12:34:56:78:90:00:00:22", + "12:34:56:78:90:00:00:23", + "12:34:56:78:90:00:00:24", + "12:34:56:78:90:00:00:25", + "12:34:56:78:90:00:00:26", + "12:34:56:78:90:00:00:27", + "12:34:56:78:90:00:00:28", + "12:34:56:78:90:00:00:29", + "12:34:56:78:90:00:00:30", + "12:34:56:78:90:00:00:31", + "12:34:56:78:90:00:00:32", + "12:34:56:78:90:00:00:33", + "12:34:56:78:90:00:00:34" + ] + }, + { + "id": "12:34:56:78:90:00:00:01", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 1", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:02", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 2", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:03", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 3", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:04", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 4", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:05", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 5", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:06", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 6", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:07", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 7", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:08", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 8", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:09", + "type": "NLPO", + "name": "OL Cumulus", + "setup_date": 1692558628, + "room_id": "1468717414", + "bridge": "bb:bb:bb:bb:bb:bb", + "appliance_type": "water_heater" + }, + { + "id": "12:34:56:78:90:00:00:10", + "type": "NLPT", + "name": "OL Lumière Cuisine", + "setup_date": 1692558628, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:11", + "type": "NLPT", + "name": "OL Escalier", + "setup_date": 1692558628, + "room_id": "3098890768", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:12", + "type": "NLPT", + "name": "OL Éclairage Salle de bain", + "setup_date": 1692558628, + "room_id": "70754041", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:13", + "type": "NLPT", + "name": "OL Lumière nouveaux toilettes", + "setup_date": 1692558628, + "room_id": "1761556353", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:14", + "type": "NLPT", + "name": "OL Lumière anciens toilettes", + "setup_date": 1692558628, + "room_id": "968846272", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:15", + "type": "NLPT", + "name": "OL Lumière palier salle de bain", + "setup_date": 1692558628, + "room_id": "4008422910", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:16", + "type": "NLPT", + "name": "OL Lumière chambre A", + "setup_date": 1692558628, + "room_id": "2640531479", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:17", + "type": "NLPT", + "name": "OL Lumière Chambre Older", + "setup_date": 1692558628, + "room_id": "2667868658", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:18", + "type": "NLPT", + "name": "OL Lumière Palier", + "setup_date": 1692558628, + "room_id": "4008422910", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:19", + "type": "NLPT", + "name": "OL Lumière enfants", + "setup_date": 1692558628, + "room_id": "3783425301", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:20", + "type": "NLM", + "name": "OL Lumière Cave", + "setup_date": 1692643741, + "room_id": "1468717414", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:21", + "type": "NLM", + "name": "OL Lumière Jardin", + "setup_date": 1692643763, + "room_id": "1156588698", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:22", + "type": "NLC", + "name": "OL Radiateur Chambre Older", + "setup_date": 1692644489, + "room_id": "2667868658", + "bridge": "bb:bb:bb:bb:bb:bb", + "appliance_type": "radiator" + }, + { + "id": "12:34:56:78:90:00:00:23", + "type": "NLC", + "name": "OL Radiateur Salon Fenêtre", + "setup_date": 1692644493, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb", + "appliance_type": "radiator" + }, + { + "id": "12:34:56:78:90:00:00:24", + "type": "NLC", + "name": "OL Radiateur Salon Porte", + "setup_date": 1692644497, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb", + "appliance_type": "radiator" + }, + { + "id": "12:34:56:78:90:00:00:25", + "type": "NLC", + "name": "OL Radiateur Chambre Old", + "setup_date": 1692644501, + "room_id": "68319658", + "bridge": "bb:bb:bb:bb:bb:bb", + "appliance_type": "radiator" + }, + { + "id": "12:34:56:78:90:00:00:26", + "type": "NLC", + "name": "OL Radiateur Serviette", + "setup_date": 1692644506, + "room_id": "70754041", + "bridge": "bb:bb:bb:bb:bb:bb", + "appliance_type": "radiator" + }, + { + "id": "12:34:56:78:90:00:00:27", + "type": "NLC", + "name": "OL Radiateur Chambre A", + "setup_date": 1692644509, + "room_id": "2640531479", + "bridge": "bb:bb:bb:bb:bb:bb", + "appliance_type": "radiator" + }, + { + "id": "12:34:56:78:90:00:00:28", + "type": "NLC", + "name": "OL Radiateur Chambre E", + "setup_date": 1693159267, + "room_id": "3783425301", + "bridge": "bb:bb:bb:bb:bb:bb", + "appliance_type": "radiator" + }, + { + "id": "12:34:56:78:90:00:00:29", + "type": "NLPT", + "name": "OL Salle à manger", + "setup_date": 1696921179, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:30", + "type": "NLPT", + "name": "OL Plan de travail", + "setup_date": 1696921199, + "room_id": "738709350", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:31", + "type": "NLAO", + "name": "Commande On/Off (Sans-Fil, Sans Pile) 9", + "setup_date": 1696922170, + "room_id": "4008422910", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:32", + "type": "NLPT", + "name": "OL Lumière Grenier", + "setup_date": 1696949709, + "room_id": "1229033409", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:33", + "type": "NLAO", + "name": "Commande Cave", + "setup_date": 1696949890, + "room_id": "1468717414", + "bridge": "bb:bb:bb:bb:bb:bb" + }, + { + "id": "12:34:56:78:90:00:00:34", + "type": "NLAO", + "name": "Commande Grenier", + "setup_date": 1696951661, + "room_id": "1229033409", + "bridge": "bb:bb:bb:bb:bb:bb" + } + ], + "temperature_control_mode": "heating", + "therm_mode": "hg", + "therm_setpoint_default_duration": 180, + "schedules": [ + { + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 420 + }, + { + "zone_id": 4, + "m_offset": 480 + }, + { + "zone_id": 0, + "m_offset": 1140 + }, + { + "zone_id": 1, + "m_offset": 1320 + }, + { + "zone_id": 0, + "m_offset": 1860 + }, + { + "zone_id": 4, + "m_offset": 1920 + }, + { + "zone_id": 0, + "m_offset": 2580 + }, + { + "zone_id": 1, + "m_offset": 2760 + }, + { + "zone_id": 0, + "m_offset": 3300 + }, + { + "zone_id": 4, + "m_offset": 3360 + }, + { + "zone_id": 0, + "m_offset": 4020 + }, + { + "zone_id": 1, + "m_offset": 4200 + }, + { + "zone_id": 0, + "m_offset": 4740 + }, + { + "zone_id": 4, + "m_offset": 4800 + }, + { + "zone_id": 0, + "m_offset": 5460 + }, + { + "zone_id": 1, + "m_offset": 5640 + }, + { + "zone_id": 0, + "m_offset": 6180 + }, + { + "zone_id": 4, + "m_offset": 6240 + }, + { + "zone_id": 0, + "m_offset": 6900 + }, + { + "zone_id": 1, + "m_offset": 7080 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 4, + "m_offset": 7800 + }, + { + "zone_id": 0, + "m_offset": 8220 + }, + { + "zone_id": 1, + "m_offset": 8520 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 4, + "m_offset": 9240 + }, + { + "zone_id": 0, + "m_offset": 9660 + }, + { + "zone_id": 1, + "m_offset": 9960 + } + ], + "zones": [ + { + "name": "Comfort", + "id": 0, + "type": 0, + "rooms_temp": [ + { + "room_id": "738709350", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "70754041", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2640531479", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2667868658", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3783425301", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "68319658", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "738709350", + "therm_setpoint_fp": "comfort" + }, + { + "id": "70754041", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2640531479", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2667868658", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3783425301", + "therm_setpoint_fp": "comfort" + }, + { + "id": "68319658", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Night", + "id": 1, + "type": 1, + "rooms_temp": [ + { + "room_id": "738709350", + "therm_setpoint_fp": "away" + }, + { + "room_id": "70754041", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2640531479", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2667868658", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3783425301", + "therm_setpoint_fp": "away" + }, + { + "room_id": "68319658", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "738709350", + "therm_setpoint_fp": "away" + }, + { + "id": "70754041", + "therm_setpoint_fp": "away" + }, + { + "id": "2640531479", + "therm_setpoint_fp": "away" + }, + { + "id": "2667868658", + "therm_setpoint_fp": "away" + }, + { + "id": "3783425301", + "therm_setpoint_fp": "away" + }, + { + "id": "68319658", + "therm_setpoint_fp": "away" + } + ] + }, + { + "name": "Comfort+", + "id": 3, + "type": 8, + "rooms_temp": [ + { + "room_id": "738709350", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "70754041", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2640531479", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "2667868658", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "3783425301", + "therm_setpoint_fp": "comfort" + }, + { + "room_id": "68319658", + "therm_setpoint_fp": "comfort" + } + ], + "modules": [], + "rooms": [ + { + "id": "738709350", + "therm_setpoint_fp": "comfort" + }, + { + "id": "70754041", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2640531479", + "therm_setpoint_fp": "comfort" + }, + { + "id": "2667868658", + "therm_setpoint_fp": "comfort" + }, + { + "id": "3783425301", + "therm_setpoint_fp": "comfort" + }, + { + "id": "68319658", + "therm_setpoint_fp": "comfort" + } + ] + }, + { + "name": "Eco", + "id": 4, + "type": 5, + "rooms_temp": [ + { + "room_id": "738709350", + "therm_setpoint_fp": "away" + }, + { + "room_id": "70754041", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2640531479", + "therm_setpoint_fp": "away" + }, + { + "room_id": "2667868658", + "therm_setpoint_fp": "away" + }, + { + "room_id": "3783425301", + "therm_setpoint_fp": "away" + }, + { + "room_id": "68319658", + "therm_setpoint_fp": "away" + } + ], + "modules": [], + "rooms": [ + { + "id": "738709350", + "therm_setpoint_fp": "away" + }, + { + "id": "70754041", + "therm_setpoint_fp": "away" + }, + { + "id": "2640531479", + "therm_setpoint_fp": "away" + }, + { + "id": "2667868658", + "therm_setpoint_fp": "away" + }, + { + "id": "3783425301", + "therm_setpoint_fp": "away" + }, + { + "id": "68319658", + "therm_setpoint_fp": "away" + } + ] + } + ], + "name": "Mon planning de chauffe", + "default": false, + "away_temp": 12, + "hg_temp": 7, + "id": "64e3b6102853d9405304b27a", + "type": "therm", + "selected": true + }, + { + "timetable": [], + "zones": [], + "name": "Planning d'actions", + "default": false, + "timetable_sunrise": [], + "timetable_sunset": [], + "id": "64ee68e2c8419b37790c5050", + "type": "event", + "selected": true + }, + { + "timetable": [ + { + "zone_id": 0, + "m_offset": 0 + } + ], + "zones": [ + { + "id": 0, + "price": 0.2516, + "price_type": "basic" + } + ], + "name": "electricity", + "default": false, + "tariff": "edf_tarif_bleu", + "tariff_option": "basic", + "power_threshold": 9, + "contract_power_unit": "kVA", + "id": "64e28390bc6d555f2d05c684", + "type": "electricity", + "selected": true + } + ] + } + ], + "user": { + "email": "john.doe@doe.com", + "language": "fr-FR", + "locale": "fr-FR", + "feel_like_algorithm": 0, + "unit_pressure": 0, + "unit_system": 0, + "unit_wind": 0, + "id": "111111111112222222333333" + } + }, + "status": "ok", + "time_exec": 0.07427000999450684, + "time_server": 1709762264 +} \ No newline at end of file diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index fe1919d1..4eee10b8 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -106,11 +106,13 @@ async def async_update_measures( start_time: int | None = None, interval: MeasureInterval = MeasureInterval.HOUR, days: int = 7, + end_time: int | None = None ) -> None: """Retrieve measures data from /getmeasure.""" await getattr(self.homes[home_id].modules[module_id], "async_update_measures")( start_time=start_time, + end_time=end_time, interval=interval, days=days, ) diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index 52d0d14a..cbaa8677 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -109,6 +109,9 @@ SCHEDULE_TYPE_COOLING = "cooling" +ENERGY_ELEC_PEAK_IDX = 0 +ENERGY_ELEC_OFF_IDX = 1 + class MeasureType(Enum): """Measure type.""" @@ -135,3 +138,10 @@ class MeasureInterval(Enum): DAY = "1day" WEEK = "1week" MONTH = "1month" + +MEASURE_INTERVAL_TO_SECONDS = {MeasureInterval.HALF_HOUR:1800, + MeasureInterval.HOUR:3600, + MeasureInterval.THREE_HOURS:10800, + MeasureInterval.DAY:86400, + MeasureInterval.WEEK:604800, + MeasureInterval.MONTH:2592000} \ No newline at end of file diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 2f64110f..6d585c3a 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -16,7 +16,7 @@ SETTHERMMODE_ENDPOINT, SWITCHHOMESCHEDULE_ENDPOINT, SYNCHOMESCHEDULE_ENDPOINT, - RawData, SCHEDULE_TYPE_THERM, SCHEDULE_TYPE_ELECTRICITY, MeasureType, + RawData, SCHEDULE_TYPE_THERM, SCHEDULE_TYPE_ELECTRICITY, MeasureType, ENERGY_ELEC_PEAK_IDX, ENERGY_ELEC_OFF_IDX, ) from pyatmo.event import Event from pyatmo.exceptions import InvalidSchedule, InvalidState, NoSchedule @@ -44,6 +44,7 @@ class Home: persons: dict[str, Person] events: dict[str, Event] energy_endpoints: list[str] + energy_schedule: list[int] def __init__(self, auth: AbstractAsyncAuth, raw_data: RawData) -> None: """Initialize a Netatmo home instance.""" @@ -85,13 +86,47 @@ def _handle_schedules(self, raw_data): nrj_schedule = next(iter(schedules.get(SCHEDULE_TYPE_ELECTRICITY, {}).values()), None) - type_tariff = None + self.energy_schedule_vals = [] + self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] if nrj_schedule is not None: type_tariff = nrj_schedule.tariff_option + zones = nrj_schedule.zones - self.energy_endpoints = {"basic": [MeasureType.SUM_ENERGY_ELEC_BASIC.value], - "peak_and_off_peak": [MeasureType.SUM_ENERGY_ELEC_PEAK.value, MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value] - }.get(type_tariff, [MeasureType.SUM_ENERGY_ELEC.value]) + if type_tariff == "peak_and_off_peak" and len(zones) >= 2: + + + + self.energy_endpoints = [None, None] + + self.energy_endpoints[ENERGY_ELEC_PEAK_IDX] = MeasureType.SUM_ENERGY_ELEC_PEAK.value + self.energy_endpoints[ENERGY_ELEC_OFF_IDX] = MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value + + if zones[0].price_type == "peak": + peak_id = zones[0].entity_id + off_peak_id = zones[1].entity_id + + else: + peak_id = zones[1].entity_id + off_peak_id = zones[0].entity_id + + timetable = nrj_schedule.timetable + + #timetable are daily for electricity type, and sorted from begining to end + for t in timetable: + + time = t.m_offset*60 #m_offset is in minute from the begininng of the day + if len(self.energy_schedule_vals) == 0: + time = 0 + + pos_to_add = ENERGY_ELEC_OFF_IDX + if t.zone_id == peak_id: + pos_to_add = ENERGY_ELEC_PEAK_IDX + + self.energy_schedule_vals.append((time,pos_to_add)) + + + else: + self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index a5f0fbbb..45babf8d 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -1,13 +1,15 @@ """Module to represent a Netatmo module.""" from __future__ import annotations +import copy from datetime import datetime, timezone, timedelta import logging from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError -from pyatmo.const import GETMEASURE_ENDPOINT, RawData, MeasureInterval +from pyatmo.const import GETMEASURE_ENDPOINT, RawData, MeasureInterval, ENERGY_ELEC_PEAK_IDX, \ + MEASURE_INTERVAL_TO_SECONDS from pyatmo.exceptions import ApiError, InvalidHistoryFromAPI from pyatmo.modules.base_class import EntityBase, NetatmoBase, Place from pyatmo.modules.device_types import DEVICE_CATEGORY_MAP, DeviceCategory, DeviceType @@ -16,6 +18,10 @@ from pyatmo.event import Event from pyatmo.home import Home + +import bisect +from operator import itemgetter + LOG = logging.getLogger(__name__) ModuleT = dict[str, Any] @@ -584,6 +590,8 @@ def __init__(self, home: Home, module: ModuleT): self.end_time: int | None = None self.interval: MeasureInterval | None = None self.sum_energy_elec: int | None = None + self.sum_energy_elec_peak: int | None = None + self.sum_energy_elec_off_peak: int | None = None def reset_measures(self): self.start_time = None @@ -592,13 +600,13 @@ def reset_measures(self): self.sum_energy_elec = 0 - + async def async_update_measures( self, start_time: int | None = None, end_time: int | None = None, interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7 + days: int = 7, ) -> None: """Update historical data.""" @@ -610,6 +618,16 @@ async def async_update_measures( start_time = end - timedelta(days=days) start_time = int(start_time.timestamp()) + + #the legrand/netatmo handling of start and endtime is very peculiar + #for 30mn/1h/3h intervals : in fact the starts is asked_start + intervals/2 ! yes so shift of 15mn, 30mn and 1h30 + #for 1day : start is ALWAYS 12am (half day) of the first day of the range + #for 1week : it will be half week ALWAYS, ie on a thursday at 12am (half day) + if interval in {MeasureInterval.HALF_HOUR, MeasureInterval.HOUR, MeasureInterval.THREE_HOURS}: + start_time -= MEASURE_INTERVAL_TO_SECONDS.get(interval, 0)//2 + + + data_points = self.home.energy_endpoints raw_datas = [] @@ -628,70 +646,148 @@ async def async_update_measures( endpoint=GETMEASURE_ENDPOINT, params=params, ) - raw_datas.append(await resp.json()) - self.historical_data = [] + rw_dt = await resp.json() + rw_dt = rw_dt.get("body") + + if rw_dt is None: + raise InvalidHistoryFromAPI(f"No energy historical data from {data_point}") + + if len(rw_dt) == 0: + raise InvalidHistoryFromAPI(f"No energy historical data from {data_point}") + + raw_datas.append(rw_dt) + + + + hist_good_vals = [] + energy_schedule_vals = [] + + peak_off_peak_mode = False + if len(raw_datas) > 1 and len(self.home.energy_schedule_vals) > 0: + peak_off_peak_mode = True + + if peak_off_peak_mode: + max_interval_sec = 0 + for cur_energy_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): + for values_lot in values_lots: + max_interval_sec = max(max_interval_sec, int(values_lot["step_time"])) + + + biggest_day_interval = (max_interval_sec)//(3600*24) + 1 + + energy_schedule_vals = copy.copy(self.home.energy_schedule_vals) - raw_datas = [raw_data["body"] for raw_data in raw_datas] + if energy_schedule_vals[-1][0] < max_interval_sec + (3600*24): + if energy_schedule_vals[0][1] == energy_schedule_vals[-1][1]: + #it means the last one continue in the first one the next day + energy_schedule_vals_next = energy_schedule_vals[1:] + else: + energy_schedule_vals_next = copy.copy(self.home.energy_schedule_vals) - data = raw_datas[0][0] + for d in range(0, biggest_day_interval): + next_day_extension = [ (offset + ((d+1)*24*3600), mode) for offset,mode in energy_schedule_vals_next] + energy_schedule_vals.extend(next_day_extension) - if len(data) == 0: - raise InvalidHistoryFromAPI(f"No energy historical data from {data_points[0]}") + for cur_energy_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): + for values_lot in values_lots: + start_lot_time = int(values_lot["beg_time"]) + interval_sec = int(values_lot["step_time"]) + cur_start_time = start_lot_time + for val_arr in values_lot.get("value",[]): + val = val_arr[0] - interval_sec = int(data["step_time"]) + cur_end_time = cur_start_time + interval_sec + next_start_time = cur_end_time + if peak_off_peak_mode: + d_srt = datetime.fromtimestamp(cur_start_time) + #offset from start of the day + day_origin = int(datetime(d_srt.year, d_srt.month, d_srt.day).timestamp()) + srt_beg = cur_start_time - day_origin - if len(raw_datas) > 1: - #check that all data are well aligned and compatible - beg_times = {} + #now check if srt_beg is in a schedule span of the right type + idx_start = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg) - for rg in raw_datas[0]: - beg_times[(int(rg["beg_time"]))] = len(rg["value"]) - for raw_data in raw_datas: - for rg in raw_data: - if int(rg["step_time"]) != interval_sec: - raise InvalidHistoryFromAPI(f"Invalid energy historical data from {data_points}, step_time mismatch") - b = int(rg["beg_time"]) - if b not in beg_times: - raise InvalidHistoryFromAPI(f"Invalid energy historical data from {data_points}, beg_time mismatch") - if beg_times[b] != len(rg["value"]): - raise InvalidHistoryFromAPI(f"Invalid energy historical data from {data_points}, value size mismatch mismatch") + if self.home.energy_schedule_vals[idx_start][1] == cur_energy_peak_or_off_peak_mode: + #adapt the end time if needed as the next one is, by construction not cur_pos hence not compatible! + idx_end = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg + interval_sec) + if energy_schedule_vals[idx_end][1] != cur_energy_peak_or_off_peak_mode: + cur_end_time = energy_schedule_vals[idx_end][0] + day_origin + else: + #we are NOT in a proper schedule time for this time span ... jump to the next one... meaning it is the next day! + if idx_start == len(energy_schedule_vals) - 1: + #should never append with the performed day extension above + raise InvalidHistoryFromAPI(f"bad formed energy history data or schedule data") + else: + #by construction of the energy schedule the next one should be of opposite mode + if energy_schedule_vals[idx_start + 1][1] != cur_energy_peak_or_off_peak_mode: + raise InvalidHistoryFromAPI(f"bad formed energy schedule data") + + start_time_to_get_closer = energy_schedule_vals[idx_start+1][0] + diff_t = start_time_to_get_closer - srt_beg + m_diff = diff_t % interval_sec + cur_start_time = day_origin + start_time_to_get_closer - m_diff + idx_end = self._get_proper_in_schedule_index(energy_schedule_vals, start_time_to_get_closer - m_diff + interval_sec) + + if energy_schedule_vals[idx_end][1] != cur_energy_peak_or_off_peak_mode: + cur_end_time = energy_schedule_vals[idx_end][0] + day_origin + else: + cur_end_time = cur_start_time + interval_sec + + next_start_time = cur_start_time + interval_sec + + + hist_good_vals.append((cur_start_time, val, cur_end_time, cur_energy_peak_or_off_peak_mode)) + cur_start_time = next_start_time + + + hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) + + self.historical_data = [] self.sum_energy_elec = 0 + self.sum_energy_elec_peak = 0 + self.sum_energy_elec_off_peak = 0 self.end_time = end_time - self.start_time = int(data["beg_time"]) - - for i_step, step_0 in enumerate(raw_datas[0]): - start_time = int(step_0["beg_time"]) - interval_sec = int(step_0["step_time"]) - interval_min = interval_sec // 60 - for i_value in range(len(step_0["value"])): - end_time = start_time + interval_sec - tot_val = 0 - vals = [] - for raw_data in raw_datas: - val = int(raw_data[i_step]["value"][i_value][0]) - tot_val += val - vals.append(val) - self.sum_energy_elec += val - self.historical_data.append( - { - "duration": interval_min, - "startTime": f"{datetime.fromtimestamp(start_time + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", - "endTime": f"{datetime.fromtimestamp(end_time, tz=timezone.utc).isoformat().split('+')[0]}Z", - "Wh": tot_val, - "allWh" : vals, - "startTimeUnix": start_time, - "endTimeUnix": end_time - - }, - ) - - start_time = end_time + + for cur_start_time, val, cur_end_time, cur_energy_peak_or_off_peak_mode in hist_good_vals: + + self.sum_energy_elec += val + + if peak_off_peak_mode: + mode = "off_peak" + if cur_energy_peak_or_off_peak_mode == ENERGY_ELEC_PEAK_IDX: + self.sum_energy_elec_peak += val + mode = "peak" + else: + self.sum_energy_elec_off_peak += val + else: + mode = "standard" + + self.historical_data.append( + { + "duration": (cur_end_time - cur_start_time)//60, + "startTime": f"{datetime.fromtimestamp(cur_start_time + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", + "endTime": f"{datetime.fromtimestamp(cur_end_time, tz=timezone.utc).isoformat().split('+')[0]}Z", + "Wh": val, + "energyMode": mode, + "startTimeUnix": cur_start_time, + "endTimeUnix": cur_end_time + + }, + ) + + def _get_proper_in_schedule_index(self, energy_schedule_vals, srt_beg): + idx = bisect.bisect_left(energy_schedule_vals, srt_beg, key=itemgetter(0)) + if idx >= len(energy_schedule_vals): + idx = len(energy_schedule_vals) - 1 + elif energy_schedule_vals[idx][0] > srt_beg: # if strict equal idx is the good one + idx = max(0, idx - 1) + return idx class Module(NetatmoBase): diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index a96c644e..03203850 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -24,6 +24,7 @@ class Schedule(NetatmoBase): default: bool type: str timetable: list[TimetableEntry] + zones: list[Zone] def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule instance.""" @@ -39,9 +40,20 @@ def __init__(self, home: Home, raw_data: RawData) -> None: +@dataclass +class ScheduleWithRealZones(Schedule): + """Class to represent a Netatmo schedule.""" + + zones: list[Zone] + + def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize a Netatmo schedule instance.""" + super().__init__(home, raw_data) + self.zones = [Zone(home, r) for r in raw_data.get("zones", [])] + @dataclass -class ThermSchedule(Schedule): +class ThermSchedule(ScheduleWithRealZones): """Class to represent a Netatmo Temperature schedule.""" away_temp: float | None @@ -63,7 +75,6 @@ class CoolingSchedule(ThermSchedule): def __init__(self, home: Home, raw_data: RawData) -> None: super().__init__(home, raw_data) - self.hg_temp = raw_data.get("hg_temp") self.cooling_away_temp = self.away_temp = raw_data.get("cooling_away_temp", self.away_temp) @dataclass @@ -74,6 +85,7 @@ class ElectricitySchedule(Schedule): tariff_option: str power_threshold: int | 6 contract_power_unit: str #kVA or KW + zones: list[ZoneElectricity] def __init__(self, home: Home, raw_data: RawData) -> None: super().__init__(home, raw_data) @@ -81,6 +93,7 @@ def __init__(self, home: Home, raw_data: RawData) -> None: self.tariff_option = raw_data.get("tariff_option", None) self.power_threshold = raw_data.get("power_threshold", 6) self.contract_power_unit = raw_data.get("power_threshold", "kVA") + self.zones = [ZoneElectricity(home, r) for r in raw_data.get("zones", [])] @@ -156,6 +169,7 @@ class Zone(NetatmoBase): rooms: list[Room] modules: list[ModuleSchedule] + def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule's zone instance.""" super().__init__(raw_data) @@ -171,6 +185,20 @@ def room_factory(home: Home, room_raw_data: RawData): self.modules = [ModuleSchedule(home, m) for m in raw_data.get("modules", [])] +@dataclass +class ZoneElectricity(NetatmoBase): + """Class to represent a Netatmo schedule's zone.""" + + price: float + price_type: str + + def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize a Netatmo schedule's zone instance.""" + super().__init__(raw_data) + self.home = home + self.price = raw_data.get("price", 0.0) + self.price_type = raw_data.get("price_type", "off_peak") + def schedule_factory(home: Home, raw_data: RawData) -> (Schedule, str): type = raw_data.get("type", "custom") diff --git a/tests/common.py b/tests/common.py index a210be34..a18847d5 100644 --- a/tests/common.py +++ b/tests/common.py @@ -60,11 +60,22 @@ async def fake_post_request(*args, **kwargs): elif endpoint == "getmeasure": module_id = kwargs.get("params", {}).get("module_id") + type = kwargs.get("params", {}).get("type") payload = json.loads( - load_fixture(f"{endpoint}_{module_id.replace(':', '_')}.json"), + load_fixture(f"{endpoint}_{type}_{module_id.replace(':', '_')}.json"), ) else: - payload = json.loads(load_fixture(f"{endpoint}.json")) + postfix = kwargs.get("POSTFIX", None) + if postfix is not None: + payload = json.loads(load_fixture(f"{endpoint}_{postfix}.json")) + else: + payload = json.loads(load_fixture(f"{endpoint}.json")) return MockResponse(payload, 200) + + +async def fake_post_request_multi(*args, **kwargs): + kwargs["POSTFIX"] = "multi" + r = await fake_post_request(*args, **kwargs) + return r diff --git a/tests/conftest.py b/tests/conftest.py index 0b0c7703..e209f309 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pyatmo import pytest -from .common import fake_post_request +from .common import fake_post_request, fake_post_request_multi @contextmanager @@ -43,3 +43,28 @@ async def async_home(async_account): home_id = "91763b24c43d3e344f424e8b" await async_account.async_update_status(home_id) yield async_account.homes[home_id] + + + +@pytest.fixture(scope="function") +async def async_account_multi(async_auth): + """AsyncAccount fixture.""" + account = pyatmo.AsyncAccount(async_auth) + + with patch( + "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", + fake_post_request_multi, + ), patch( + "pyatmo.auth.AbstractAsyncAuth.async_post_request", + fake_post_request_multi, + ): + await account.async_update_topology() + yield account + + +@pytest.fixture(scope="function") +async def async_home_multi(async_account_multi): + """AsyncClimate fixture for home_id 91763b24c43d3e344f424e8b.""" + home_id = "aaaaaaaaaaabbbbbbbbbbccc" + await async_account.async_update_status(home_id) + yield async_account.homes[home_id] diff --git a/tests/test_energy.py b/tests/test_energy.py index 06ec2408..053f5c19 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -3,6 +3,12 @@ from pyatmo import DeviceType import pytest +import time_machine +import datetime as dt + +from pyatmo.const import MeasureInterval + + # pylint: disable=F6401 @@ -14,3 +20,69 @@ async def test_async_energy_NLPC(async_home): # pylint: disable=invalid-name module = async_home.modules[module_id] assert module.device_type == DeviceType.NLPC assert module.power == 476 + +@time_machine.travel(dt.datetime(2022, 2, 12, 7, 59, 49)) +@pytest.mark.asyncio +async def test_historical_data_retrieval(async_account): + """Test retrieval of historical measurements.""" + home_id = "91763b24c43d3e344f424e8b" + await async_account.async_update_events(home_id=home_id) + home = async_account.homes[home_id] + + module_id = "12:34:56:00:00:a1:4c:da" + assert module_id in home.modules + module = home.modules[module_id] + assert module.device_type == DeviceType.NLPC + + await async_account.async_update_measures(home_id=home_id, module_id=module_id) + assert module.historical_data[0] == { + "Wh": 197, + "duration": 60, + "startTime": "2022-02-05T08:29:50Z", + "endTime": "2022-02-05T09:29:49Z", + "endTimeUnix": 1644053389, + "startTimeUnix": 1644049789, + "energyMode": "standard" + } + assert module.historical_data[-1] == { + "Wh": 259, + "duration": 60, + "startTime": "2022-02-12T07:29:50Z", + "startTimeUnix": 1644650989, + "endTime": "2022-02-12T08:29:49Z", + "endTimeUnix": 1644654589, + "energyMode": "standard" + } + assert len(module.historical_data) == 168 + + + +async def test_historical_data_retrieval_multi(async_account_multi): + """Test retrieval of historical measurements.""" + home_id = "aaaaaaaaaaabbbbbbbbbbccc" + + home = async_account_multi.homes[home_id] + + module_id = "98:76:54:32:10:00:00:73" + assert module_id in home.modules + module = home.modules[module_id] + assert module.device_type == DeviceType.NLC + + strt = 1709421900 #1709421000+15*60 + await async_account_multi.async_update_measures(home_id=home_id, + module_id=module_id, + interval=MeasureInterval.HALF_HOUR, + start_time=strt, + end_time=1709679599 + ) + + + assert module.historical_data[0] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-02T23:55:00Z', 'endTimeUnix': 1709423700, 'energyMode': 'peak', 'startTime': '2024-03-02T23:25:01Z', 'startTimeUnix': strt} + assert module.historical_data[-1] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-05T20:55:00Z', 'endTimeUnix': 1709672100, 'energyMode': 'peak', 'startTime': '2024-03-05T20:25:01Z', 'startTimeUnix': 1709670300} + assert len(module.historical_data) == 134 + + assert module.sum_energy_elec == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak + assert module.sum_energy_elec_off_peak == 11219 + assert module.sum_energy_elec_peak == 31282 + + diff --git a/tests/test_home.py b/tests/test_home.py index 92c32556..ad103a08 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -1,12 +1,12 @@ """Define tests for home module.""" -import datetime as dt + import json from unittest.mock import AsyncMock, patch import pyatmo from pyatmo import DeviceType, NoDevice import pytest -import time_machine + from tests.common import MockResponse @@ -144,40 +144,6 @@ async def test_home_event_update(async_account): assert events[1].event_type == "connection" -@time_machine.travel(dt.datetime(2022, 2, 12, 7, 59, 49)) -@pytest.mark.asyncio -async def test_historical_data_retrieval(async_account): - """Test retrieval of historical measurements.""" - home_id = "91763b24c43d3e344f424e8b" - await async_account.async_update_events(home_id=home_id) - home = async_account.homes[home_id] - - module_id = "12:34:56:00:00:a1:4c:da" - assert module_id in home.modules - module = home.modules[module_id] - assert module.device_type == DeviceType.NLPC - - await async_account.async_update_measures(home_id=home_id, module_id=module_id) - assert module.historical_data[0] == { - "Wh": 197, - "allWh": [197], - "duration": 60, - "startTime": "2022-02-05T08:29:50Z", - "endTime": "2022-02-05T09:29:49Z", - "endTimeUnix": 1644053389, - "startTimeUnix": 1644049789 - } - assert module.historical_data[-1] == { - "Wh": 259, - 'allWh': [259], - "duration": 60, - "startTime": "2022-02-12T07:29:50Z", - "startTimeUnix": 1644650989, - "endTime": "2022-02-12T08:29:49Z", - "endTimeUnix": 1644654589, - } - assert len(module.historical_data) == 168 - def test_device_types_missing(): """Test handling of missing device types.""" From e8f3065f9de8c3634cf1871ca9fa1b21a014ee08 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 10 Mar 2024 02:42:45 +0100 Subject: [PATCH 08/97] Fixed correctly the timing end/start of measures --- src/pyatmo/modules/module.py | 44 ++++++++++------------------ tests/test_energy.py | 30 +++++-------------- tests/testing_main_template.py | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 51 deletions(-) create mode 100644 tests/testing_main_template.py diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 45babf8d..632c4bf3 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -619,12 +619,14 @@ async def async_update_measures( start_time = int(start_time.timestamp()) + delta_range = 0 #the legrand/netatmo handling of start and endtime is very peculiar #for 30mn/1h/3h intervals : in fact the starts is asked_start + intervals/2 ! yes so shift of 15mn, 30mn and 1h30 #for 1day : start is ALWAYS 12am (half day) of the first day of the range #for 1week : it will be half week ALWAYS, ie on a thursday at 12am (half day) - if interval in {MeasureInterval.HALF_HOUR, MeasureInterval.HOUR, MeasureInterval.THREE_HOURS}: - start_time -= MEASURE_INTERVAL_TO_SECONDS.get(interval, 0)//2 + + #in fact in the case for all intervals the reported dates are "the middle" of the ranges + delta_range = MEASURE_INTERVAL_TO_SECONDS.get(interval, 0)//2 @@ -642,6 +644,7 @@ async def async_update_measures( "date_end": end_time, } + resp = await self.home.auth.async_post_api_request( endpoint=GETMEASURE_ENDPOINT, params=params, @@ -698,8 +701,6 @@ async def async_update_measures( for val_arr in values_lot.get("value",[]): val = val_arr[0] - cur_end_time = cur_start_time + interval_sec - next_start_time = cur_end_time if peak_off_peak_mode: @@ -711,13 +712,8 @@ async def async_update_measures( #now check if srt_beg is in a schedule span of the right type idx_start = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg) + if self.home.energy_schedule_vals[idx_start][1] != cur_energy_peak_or_off_peak_mode: - if self.home.energy_schedule_vals[idx_start][1] == cur_energy_peak_or_off_peak_mode: - #adapt the end time if needed as the next one is, by construction not cur_pos hence not compatible! - idx_end = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg + interval_sec) - if energy_schedule_vals[idx_end][1] != cur_energy_peak_or_off_peak_mode: - cur_end_time = energy_schedule_vals[idx_end][0] + day_origin - else: #we are NOT in a proper schedule time for this time span ... jump to the next one... meaning it is the next day! if idx_start == len(energy_schedule_vals) - 1: #should never append with the performed day extension above @@ -729,20 +725,10 @@ async def async_update_measures( start_time_to_get_closer = energy_schedule_vals[idx_start+1][0] diff_t = start_time_to_get_closer - srt_beg - m_diff = diff_t % interval_sec - cur_start_time = day_origin + start_time_to_get_closer - m_diff - idx_end = self._get_proper_in_schedule_index(energy_schedule_vals, start_time_to_get_closer - m_diff + interval_sec) - - if energy_schedule_vals[idx_end][1] != cur_energy_peak_or_off_peak_mode: - cur_end_time = energy_schedule_vals[idx_end][0] + day_origin - else: - cur_end_time = cur_start_time + interval_sec - - next_start_time = cur_start_time + interval_sec - + cur_start_time = day_origin + srt_beg + (diff_t//interval_sec + 1)*interval_sec - hist_good_vals.append((cur_start_time, val, cur_end_time, cur_energy_peak_or_off_peak_mode)) - cur_start_time = next_start_time + hist_good_vals.append((cur_start_time, val, cur_energy_peak_or_off_peak_mode)) + cur_start_time = cur_start_time + interval_sec hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) @@ -754,7 +740,7 @@ async def async_update_measures( self.sum_energy_elec_off_peak = 0 self.end_time = end_time - for cur_start_time, val, cur_end_time, cur_energy_peak_or_off_peak_mode in hist_good_vals: + for cur_start_time, val, cur_energy_peak_or_off_peak_mode in hist_good_vals: self.sum_energy_elec += val @@ -770,13 +756,13 @@ async def async_update_measures( self.historical_data.append( { - "duration": (cur_end_time - cur_start_time)//60, - "startTime": f"{datetime.fromtimestamp(cur_start_time + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", - "endTime": f"{datetime.fromtimestamp(cur_end_time, tz=timezone.utc).isoformat().split('+')[0]}Z", + "duration": (2*delta_range)//60, + "startTime": f"{datetime.fromtimestamp(cur_start_time - delta_range + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", + "endTime": f"{datetime.fromtimestamp(cur_start_time + delta_range, tz=timezone.utc).isoformat().split('+')[0]}Z", "Wh": val, "energyMode": mode, - "startTimeUnix": cur_start_time, - "endTimeUnix": cur_end_time + "startTimeUnix": cur_start_time - delta_range, + "endTimeUnix": cur_start_time + delta_range }, ) diff --git a/tests/test_energy.py b/tests/test_energy.py index 053f5c19..186e41c5 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -35,24 +35,9 @@ async def test_historical_data_retrieval(async_account): assert module.device_type == DeviceType.NLPC await async_account.async_update_measures(home_id=home_id, module_id=module_id) - assert module.historical_data[0] == { - "Wh": 197, - "duration": 60, - "startTime": "2022-02-05T08:29:50Z", - "endTime": "2022-02-05T09:29:49Z", - "endTimeUnix": 1644053389, - "startTimeUnix": 1644049789, - "energyMode": "standard" - } - assert module.historical_data[-1] == { - "Wh": 259, - "duration": 60, - "startTime": "2022-02-12T07:29:50Z", - "startTimeUnix": 1644650989, - "endTime": "2022-02-12T08:29:49Z", - "endTimeUnix": 1644654589, - "energyMode": "standard" - } + #changed teh reference here as start and stop data was not calculated in the spirit of the netatmo api where their time data is in the fact representing the "middle" of the range and not the begining + assert module.historical_data[0] == {'Wh': 197, 'duration': 60, 'endTime': '2022-02-05T08:59:49Z', 'endTimeUnix': 1644051589, 'energyMode': 'standard', 'startTime': '2022-02-05T07:59:50Z', 'startTimeUnix': 1644047989} + assert module.historical_data[-1] == {'Wh': 259, 'duration': 60, 'endTime': '2022-02-12T07:59:49Z', 'endTimeUnix': 1644652789, 'energyMode': 'standard', 'startTime': '2022-02-12T06:59:50Z', 'startTimeUnix': 1644649189} assert len(module.historical_data) == 168 @@ -68,17 +53,18 @@ async def test_historical_data_retrieval_multi(async_account_multi): module = home.modules[module_id] assert module.device_type == DeviceType.NLC - strt = 1709421900 #1709421000+15*60 + strt = 1709421000 + end_time = 1709679599 await async_account_multi.async_update_measures(home_id=home_id, module_id=module_id, interval=MeasureInterval.HALF_HOUR, start_time=strt, - end_time=1709679599 + end_time=end_time ) - assert module.historical_data[0] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-02T23:55:00Z', 'endTimeUnix': 1709423700, 'energyMode': 'peak', 'startTime': '2024-03-02T23:25:01Z', 'startTimeUnix': strt} - assert module.historical_data[-1] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-05T20:55:00Z', 'endTimeUnix': 1709672100, 'energyMode': 'peak', 'startTime': '2024-03-05T20:25:01Z', 'startTimeUnix': 1709670300} + assert module.historical_data[0] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-02T23:40:00Z', 'endTimeUnix': 1709422800, 'energyMode': 'peak', 'startTime': '2024-03-02T23:10:01Z', 'startTimeUnix': 1709421000} + assert module.historical_data[-1] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-05T23:10:00Z', 'endTimeUnix': 1709680200, 'energyMode': 'peak', 'startTime': '2024-03-05T22:40:01Z', 'startTimeUnix': 1709678400} assert len(module.historical_data) == 134 assert module.sum_energy_elec == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak diff --git a/tests/testing_main_template.py b/tests/testing_main_template.py new file mode 100644 index 00000000..986b22a9 --- /dev/null +++ b/tests/testing_main_template.py @@ -0,0 +1,53 @@ +import pyatmo +import aiohttp +from pyatmo.auth import AbstractAsyncAuth +from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError +import asyncio + +from pyatmo.const import MeasureInterval + + + +MY_TOKEN_FROM_NETATMO = "MY_TOKEN" + +class MyAuth(AbstractAsyncAuth): + + async def async_get_access_token(self): + + return MY_TOKEN_FROM_NETATMO + + +async def main(): + session = ClientSession() + async_auth = MyAuth(session) + account = pyatmo.AsyncAccount(async_auth) + + t = asyncio.create_task(account.async_update_topology()) + home_id = "MY_HOME_ID" + module_id = "MY_MODULE_ID" + + await asyncio.gather(t) + + await account.async_update_status(home_id=home_id) + + strt = 1709766000 + 10*60#1709421000+15*60 + end = 1709852400+10*60 + await account.async_update_measures(home_id=home_id, + module_id=module_id, + interval=MeasureInterval.HALF_HOUR, + start_time=strt, + end_time=end + ) + + + print(account) + + + +if __name__ == "__main__": + + topology = asyncio.run(main()) + + print(topology) + + From 628e610475f439d085391f9fdc7d20f75b51b3da Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 10 Mar 2024 03:57:25 +0100 Subject: [PATCH 09/97] Fixed correctly the timing end/start of measures --- src/pyatmo/modules/legrand.py | 10 +++++----- src/pyatmo/modules/module.py | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 4d00907b..480ab34c 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -17,7 +17,7 @@ ShutterMixin, Switch, SwitchMixin, - WifiMixin, + WifiMixin, DimmableMixin, ) LOG = logging.getLogger(__name__) @@ -29,8 +29,8 @@ class NLG(FirmwareMixin, OffloadMixin, WifiMixin, Module): """Legrand gateway.""" -class NLT(FirmwareMixin, BatteryMixin, Module): - """Legrand global remote control.""" +class NLT(DimmableMixin, FirmwareMixin, BatteryMixin, SwitchMixin, Module): + """Legrand global remote control...but also wireless switch, like NLD""" class NLP(Switch, OffloadMixin): @@ -73,8 +73,8 @@ class NLIS(Switch): """Legrand double switch.""" -class NLD(Dimmer): - """Legrand Double On/Off dimmer remote.""" +class NLD(DimmableMixin, FirmwareMixin, BatteryMixin, SwitchMixin, Module): + """Legrand Double On/Off dimmer remote. Wireless 2 button switch light""" class NLL(Switch, WifiMixin): diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 632c4bf3..2582d78e 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -618,8 +618,6 @@ async def async_update_measures( start_time = end - timedelta(days=days) start_time = int(start_time.timestamp()) - - delta_range = 0 #the legrand/netatmo handling of start and endtime is very peculiar #for 30mn/1h/3h intervals : in fact the starts is asked_start + intervals/2 ! yes so shift of 15mn, 30mn and 1h30 #for 1day : start is ALWAYS 12am (half day) of the first day of the range @@ -650,13 +648,15 @@ async def async_update_measures( params=params, ) - rw_dt = await resp.json() - rw_dt = rw_dt.get("body") + rw_dt_f = await resp.json() + rw_dt = rw_dt_f.get("body") if rw_dt is None: + LOG.debug("Bad Energy Response for %s, %s", self.name,rw_dt_f) raise InvalidHistoryFromAPI(f"No energy historical data from {data_point}") if len(rw_dt) == 0: + LOG.debug("Empty Energy Response %s %s", self.name,rw_dt_f) raise InvalidHistoryFromAPI(f"No energy historical data from {data_point}") raw_datas.append(rw_dt) @@ -767,6 +767,8 @@ async def async_update_measures( }, ) + LOG.debug("=> Succes in energty update %s", self.name) + def _get_proper_in_schedule_index(self, energy_schedule_vals, srt_beg): idx = bisect.bisect_left(energy_schedule_vals, srt_beg, key=itemgetter(0)) if idx >= len(energy_schedule_vals): From 08ac5f692e71854ea5f607e8c76d64c97f616ac1 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 10 Mar 2024 04:08:01 +0100 Subject: [PATCH 10/97] Fixed correctly the timing end/start of measures --- src/pyatmo/modules/module.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 2582d78e..77c938f8 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -626,7 +626,7 @@ async def async_update_measures( #in fact in the case for all intervals the reported dates are "the middle" of the ranges delta_range = MEASURE_INTERVAL_TO_SECONDS.get(interval, 0)//2 - + num_calls = 0 data_points = self.home.energy_endpoints raw_datas = [] @@ -658,7 +658,7 @@ async def async_update_measures( if len(rw_dt) == 0: LOG.debug("Empty Energy Response %s %s", self.name,rw_dt_f) raise InvalidHistoryFromAPI(f"No energy historical data from {data_point}") - + num_calls +=1 raw_datas.append(rw_dt) @@ -768,6 +768,7 @@ async def async_update_measures( ) LOG.debug("=> Succes in energty update %s", self.name) + return num_calls def _get_proper_in_schedule_index(self, energy_schedule_vals, srt_beg): idx = bisect.bisect_left(energy_schedule_vals, srt_beg, key=itemgetter(0)) From cf198ec5e8600e46c30c25ea496ae9caf6fa26cc Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 10 Mar 2024 04:47:38 +0100 Subject: [PATCH 11/97] support correct empty measures --- src/pyatmo/modules/module.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 77c938f8..bb77c262 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -655,9 +655,6 @@ async def async_update_measures( LOG.debug("Bad Energy Response for %s, %s", self.name,rw_dt_f) raise InvalidHistoryFromAPI(f"No energy historical data from {data_point}") - if len(rw_dt) == 0: - LOG.debug("Empty Energy Response %s %s", self.name,rw_dt_f) - raise InvalidHistoryFromAPI(f"No energy historical data from {data_point}") num_calls +=1 raw_datas.append(rw_dt) From 3af2cd3e325fb1d0f9a43bcbc54211c879e22b8e Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 10 Mar 2024 12:11:54 +0100 Subject: [PATCH 12/97] removed specidif energy exception to use th egeneric API error, 0lus some logging --- src/pyatmo/exceptions.py | 6 ---- src/pyatmo/modules/module.py | 57 +++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/pyatmo/exceptions.py b/src/pyatmo/exceptions.py index 146452e0..4ed5f120 100644 --- a/src/pyatmo/exceptions.py +++ b/src/pyatmo/exceptions.py @@ -41,9 +41,3 @@ class InvalidState(Exception): """Raised when an invalid state is encountered.""" pass - - -class InvalidHistoryFromAPI(Exception): - """Raised when an invalid state is encountered.""" - - pass \ No newline at end of file diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index bb77c262..0872e9c4 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -10,7 +10,7 @@ from pyatmo.const import GETMEASURE_ENDPOINT, RawData, MeasureInterval, ENERGY_ELEC_PEAK_IDX, \ MEASURE_INTERVAL_TO_SECONDS -from pyatmo.exceptions import ApiError, InvalidHistoryFromAPI +from pyatmo.exceptions import ApiError from pyatmo.modules.base_class import EntityBase, NetatmoBase, Place from pyatmo.modules.device_types import DEVICE_CATEGORY_MAP, DeviceCategory, DeviceType @@ -599,7 +599,14 @@ def reset_measures(self): self.historical_data = [] self.sum_energy_elec = 0 - + def _log_energy_error(self, start_time, end_time, msg=None, body=None): + if body is None: + body = "NO BODY" + LOG.debug("!!!!!!!!! ENERGY error %s %s %s %s", + msg, + self.name, + datetime.fromtimestamp(start_time), + datetime.fromtimestamp(end_time)), start_time, end_time, body async def async_update_measures( self, @@ -607,7 +614,7 @@ async def async_update_measures( end_time: int | None = None, interval: MeasureInterval = MeasureInterval.HOUR, days: int = 7, - ) -> None: + ) -> int | None: """Update historical data.""" if end_time is None: @@ -652,8 +659,12 @@ async def async_update_measures( rw_dt = rw_dt_f.get("body") if rw_dt is None: - LOG.debug("Bad Energy Response for %s, %s", self.name,rw_dt_f) - raise InvalidHistoryFromAPI(f"No energy historical data from {data_point}") + self._log_energy_error(start_time, end_time, msg=f"direct from {data_point}", body=rw_dt_f) + raise ApiError( + f"Energy badly formed resp: {rw_dt_f} - " + f"module: {self.name} - " + f"when accessing '{data_point}'" + ) num_calls +=1 raw_datas.append(rw_dt) @@ -689,11 +700,28 @@ async def async_update_measures( next_day_extension = [ (offset + ((d+1)*24*3600), mode) for offset,mode in energy_schedule_vals_next] energy_schedule_vals.extend(next_day_extension) + interval_sec = 2*delta_range for cur_energy_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: - start_lot_time = int(values_lot["beg_time"]) - interval_sec = int(values_lot["step_time"]) + try: + start_lot_time = int(values_lot["beg_time"]) + except: + self._log_energy_error(start_time, end_time, + msg=f"beg_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", + body=raw_datas[cur_energy_peak_or_off_peak_mode]) + raise ApiError( + f"Energy badly formed resp beg_time missing: {raw_datas[cur_energy_peak_or_off_peak_mode]} - " + f"module: {self.name} - " + f"when accessing '{data_points[cur_energy_peak_or_off_peak_mode]}'" + ) + + try: + interval_sec = int(values_lot["step_time"]) + except: + self._log_energy_error(start_time, end_time, msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", body=raw_datas[cur_energy_peak_or_off_peak_mode]) + #maybe we contineu with default step time? + cur_start_time = start_lot_time for val_arr in values_lot.get("value",[]): val = val_arr[0] @@ -714,11 +742,20 @@ async def async_update_measures( #we are NOT in a proper schedule time for this time span ... jump to the next one... meaning it is the next day! if idx_start == len(energy_schedule_vals) - 1: #should never append with the performed day extension above - raise InvalidHistoryFromAPI(f"bad formed energy history data or schedule data") + self._log_energy_error(start_time, end_time, + msg=f"bad idx missing {data_points[cur_energy_peak_or_off_peak_mode]}", + body=raw_datas[cur_energy_peak_or_off_peak_mode]) + + return 0 else: #by construction of the energy schedule the next one should be of opposite mode if energy_schedule_vals[idx_start + 1][1] != cur_energy_peak_or_off_peak_mode: - raise InvalidHistoryFromAPI(f"bad formed energy schedule data") + self._log_energy_error(start_time, end_time, + msg=f"bad schedule {data_points[cur_energy_peak_or_off_peak_mode]}", + body=raw_datas[cur_energy_peak_or_off_peak_mode]) + return 0 + + start_time_to_get_closer = energy_schedule_vals[idx_start+1][0] diff_t = start_time_to_get_closer - srt_beg @@ -764,7 +801,7 @@ async def async_update_measures( }, ) - LOG.debug("=> Succes in energty update %s", self.name) + LOG.debug("=> Success in energyy update %s", self.name) return num_calls def _get_proper_in_schedule_index(self, energy_schedule_vals, srt_beg): From e4139111918c68e41de5e75033d330f23335448b Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 10 Mar 2024 18:20:22 +0100 Subject: [PATCH 13/97] removed specidif energy exception to use th egeneric API error, 0lus some logging --- src/pyatmo/modules/module.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 0872e9c4..84a1b64a 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -606,7 +606,7 @@ def _log_energy_error(self, start_time, end_time, msg=None, body=None): msg, self.name, datetime.fromtimestamp(start_time), - datetime.fromtimestamp(end_time)), start_time, end_time, body + datetime.fromtimestamp(end_time), start_time, end_time, body) async def async_update_measures( self, @@ -638,6 +638,8 @@ async def async_update_measures( data_points = self.home.energy_endpoints raw_datas = [] + LOG.debug("INFO: doing async_update_measures for %", self.name) + for data_point in data_points: params = { @@ -678,11 +680,19 @@ async def async_update_measures( if len(raw_datas) > 1 and len(self.home.energy_schedule_vals) > 0: peak_off_peak_mode = True + interval_sec = 2 * delta_range + if peak_off_peak_mode: - max_interval_sec = 0 + max_interval_sec = interval_sec for cur_energy_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: - max_interval_sec = max(max_interval_sec, int(values_lot["step_time"])) + try: + max_interval_sec = max(max_interval_sec, int(values_lot["step_time"])) + except: + self._log_energy_error(start_time, end_time, + msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", + body=raw_datas[cur_energy_peak_or_off_peak_mode]) + # maybe we continue with default step time? or do we have an error? biggest_day_interval = (max_interval_sec)//(3600*24) + 1 @@ -700,7 +710,7 @@ async def async_update_measures( next_day_extension = [ (offset + ((d+1)*24*3600), mode) for offset,mode in energy_schedule_vals_next] energy_schedule_vals.extend(next_day_extension) - interval_sec = 2*delta_range + for cur_energy_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: From 4765304ce40c564900e9d4777f445ae498276558 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 10 Mar 2024 21:09:02 +0100 Subject: [PATCH 14/97] removed specidif energy exception to use th egeneric API error, 0lus some logging --- src/pyatmo/account.py | 74 ++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 4eee10b8..f80c8cc7 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -1,6 +1,7 @@ """Support for a Netatmo account.""" from __future__ import annotations +import copy import logging from typing import TYPE_CHECKING, Any from uuid import uuid4 @@ -30,11 +31,14 @@ class AsyncAccount: """Async class of a Netatmo account.""" - def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True) -> None: + def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True, support_only_homes: list | None = None) -> None: """Initialize the Netatmo account.""" self.auth: AbstractAsyncAuth = auth self.user: str | None = None + self.support_only_homes = support_only_homes + self.all_account_homes: dict[str, Home] = {} + self.additional_public_homes: dict[str, Home] = {} self.homes: dict[str, Home] = {} self.raw_data: RawData = {} self.favorite_stations: bool = favorite_stations @@ -48,14 +52,41 @@ def __repr__(self) -> str: f"{self.__class__.__name__}(user={self.user}, home_ids={self.homes.keys()}" ) + def update_supported_homes(self, support_only_homes: list | None = None): + + self.support_only_homes = support_only_homes + if support_only_homes is None or len(support_only_homes) == 0: + self.homes = copy.copy(self.all_account_homes) + else: + self.homes = {} + for h_id in support_only_homes: + h = self.all_account_homes.get(h_id) + if h is not None: + self.homes[h_id] = h + + if len(self.homes) == 0: + self.support_only_homes = None + self.homes = copy.copy(self.all_account_homes) + + self.homes.update(self.additional_public_homes) + + def process_topology(self) -> None: """Process topology information from /homesdata.""" for home in self.raw_data["homes"]: - if (home_id := home["id"]) in self.homes: - self.homes[home_id].update_topology(home) + if (home_id := home["id"]) in self.all_account_homes: + self.all_account_homes[home_id].update_topology(home) else: - self.homes[home_id] = Home(self.auth, raw_data=home) + self.all_account_homes[home_id] = Home(self.auth, raw_data=home) + + self.update_supported_homes(self.support_only_homes) + + def find_from_all_homes(self, home_id): + home = self.all_account_homes.get(home_id) + if home is None: + home = self.additional_public_homes.get(home_id) + return home async def async_update_topology(self) -> None: """Retrieve topology data from /homesdata.""" @@ -76,7 +107,7 @@ async def async_update_status(self, home_id: str) -> None: params={"home_id": home_id}, ) raw_data = extract_raw_data(await resp.json(), HOME) - await self.homes[home_id].update(raw_data) + await self.all_account_homes[home_id].update(raw_data) async def async_update_events(self, home_id: str) -> None: """Retrieve events from /getevents.""" @@ -85,7 +116,7 @@ async def async_update_events(self, home_id: str) -> None: params={"home_id": home_id}, ) raw_data = extract_raw_data(await resp.json(), HOME) - await self.homes[home_id].update(raw_data) + await self.all_account_homes[home_id].update(raw_data) async def async_update_weather_stations(self) -> None: """Retrieve status data from /getstationsdata.""" @@ -110,7 +141,7 @@ async def async_update_measures( ) -> None: """Retrieve measures data from /getmeasure.""" - await getattr(self.homes[home_id].modules[module_id], "async_update_measures")( + await getattr(self.find_from_all_homes(home_id).modules[module_id], "async_update_measures")( start_time=start_time, end_time=end_time, interval=interval, @@ -199,7 +230,9 @@ async def update_devices( "home_id", self.find_home_of_device(device_data), ): - if home_id not in self.homes: + home = self.find_from_all_homes(home_id) + + if home is None: modules_data = [] for module_data in device_data.get("modules", []): module_data["home_id"] = home_id @@ -208,7 +241,7 @@ async def update_devices( modules_data.append(normalize_weather_attributes(module_data)) modules_data.append(normalize_weather_attributes(device_data)) - self.homes[home_id] = Home( + home = Home( self.auth, raw_data={ "id": home_id, @@ -216,12 +249,16 @@ async def update_devices( "modules": modules_data, }, ) - await self.homes[home_id].update( + + self.additional_public_homes[home_id] = home + await home.update( {HOME: {"modules": [normalize_weather_attributes(device_data)]}}, ) else: LOG.debug("No home %s found.", home_id) + self.update_supported_homes(self.support_only_homes) + for module_data in device_data.get("modules", []): module_data["home_id"] = home_id await self.update_devices({"devices": [module_data]}) @@ -255,14 +292,15 @@ async def update_devices( def find_home_of_device(self, device_data: dict[str, Any]) -> str | None: """Find home_id of device.""" - return next( - ( - home_id - for home_id, home in self.homes.items() - if device_data["_id"] in home.modules - ), - None, - ) + for home_id, home in self.all_account_homes.items(): + if device_data["_id"] in home.modules: + return home_id + + for home_id, home in self.additional_public_homes.items(): + if device_data["_id"] in home.modules: + return home_id + + return None ATTRIBUTES_TO_FIX = { From 9efb0f3fc76a1fa6247ea20f46d7734618909014 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 11 Mar 2024 15:03:18 +0100 Subject: [PATCH 15/97] removed specidif energy exception to use th egeneric API error, 0lus some logging --- src/pyatmo/modules/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 84a1b64a..0cede275 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -638,7 +638,7 @@ async def async_update_measures( data_points = self.home.energy_endpoints raw_datas = [] - LOG.debug("INFO: doing async_update_measures for %", self.name) + LOG.debug("INFO: doing async_update_measures for %s", self.name) for data_point in data_points: From fdce0cc85a1211f3618e3874b0373aa39e2658ad Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 11 Mar 2024 20:54:30 +0100 Subject: [PATCH 16/97] async update for all homes --- src/pyatmo/account.py | 22 +++++++++++++++------- tests/conftest.py | 6 +++--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index f80c8cc7..9cbe0e45 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -100,14 +100,22 @@ async def async_update_topology(self) -> None: self.process_topology() - async def async_update_status(self, home_id: str) -> None: + async def async_update_status(self, home_id: str | None) -> None: """Retrieve status data from /homestatus.""" - resp = await self.auth.async_post_api_request( - endpoint=GETHOMESTATUS_ENDPOINT, - params={"home_id": home_id}, - ) - raw_data = extract_raw_data(await resp.json(), HOME) - await self.all_account_homes[home_id].update(raw_data) + + if home_id is None: + self.update_supported_homes(self.support_only_homes) + homes = self.homes + else: + homes = [home_id] + + for h_id in homes: + resp = await self.auth.async_post_api_request( + endpoint=GETHOMESTATUS_ENDPOINT, + params={"home_id": h_id}, + ) + raw_data = extract_raw_data(await resp.json(), HOME) + await self.all_account_homes[h_id].update(raw_data) async def async_update_events(self, home_id: str) -> None: """Retrieve events from /getevents.""" diff --git a/tests/conftest.py b/tests/conftest.py index e209f309..b42ca8fe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,7 +49,7 @@ async def async_home(async_account): @pytest.fixture(scope="function") async def async_account_multi(async_auth): """AsyncAccount fixture.""" - account = pyatmo.AsyncAccount(async_auth) + account = pyatmo.AsyncAccount(async_auth, support_only_homes=["aaaaaaaaaaabbbbbbbbbbccc"]) with patch( "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", @@ -66,5 +66,5 @@ async def async_account_multi(async_auth): async def async_home_multi(async_account_multi): """AsyncClimate fixture for home_id 91763b24c43d3e344f424e8b.""" home_id = "aaaaaaaaaaabbbbbbbbbbccc" - await async_account.async_update_status(home_id) - yield async_account.homes[home_id] + await async_account_multi.async_update_status(home_id) + yield async_account_multi.homes[home_id] From 2407b39af02ab240f59a699d0b472513b32728c9 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 12 Mar 2024 12:03:34 +0100 Subject: [PATCH 17/97] async update for all homes --- src/pyatmo/modules/module.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 0cede275..9a4365da 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -625,6 +625,13 @@ async def async_update_measures( start_time = end - timedelta(days=days) start_time = int(start_time.timestamp()) + + prev_start_time = self.start_time + prev_end_time = self.end_time + + self.start_time = start_time + self.end_time = end_time + #the legrand/netatmo handling of start and endtime is very peculiar #for 30mn/1h/3h intervals : in fact the starts is asked_start + intervals/2 ! yes so shift of 15mn, 30mn and 1h30 #for 1day : start is ALWAYS 12am (half day) of the first day of the range @@ -779,11 +786,14 @@ async def async_update_measures( self.historical_data = [] + prev_sum_energy_elec = self.sum_energy_elec self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 self.end_time = end_time + computed_start = 0 + computed_end = 0 for cur_start_time, val, cur_energy_peak_or_off_peak_mode in hist_good_vals: self.sum_energy_elec += val @@ -798,6 +808,11 @@ async def async_update_measures( else: mode = "standard" + if computed_start == 0: + computed_start = cur_start_time - delta_range + computed_end = cur_start_time + delta_range + + self.historical_data.append( { "duration": (2*delta_range)//60, @@ -811,7 +826,14 @@ async def async_update_measures( }, ) - LOG.debug("=> Success in energyy update %s", self.name) + LOG.debug("=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec) + if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: + LOG.debug( + ">>>>>>>>>> ENERGY GOING DOWN %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f", + self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), + datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, + prev_sum_energy_elec) + return num_calls def _get_proper_in_schedule_index(self, energy_schedule_vals, srt_beg): From 9e82e1ac3b8a52da25acc5e04248a85f9c55ecaa Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 13 Mar 2024 01:01:33 +0100 Subject: [PATCH 18/97] async update for all homes --- src/pyatmo/account.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 9cbe0e45..02c3b9a9 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -100,7 +100,7 @@ async def async_update_topology(self) -> None: self.process_topology() - async def async_update_status(self, home_id: str | None) -> None: + async def async_update_status(self, home_id: str | None = None) -> int: """Retrieve status data from /homestatus.""" if home_id is None: @@ -108,7 +108,7 @@ async def async_update_status(self, home_id: str | None) -> None: homes = self.homes else: homes = [home_id] - + num_calls = 0 for h_id in homes: resp = await self.auth.async_post_api_request( endpoint=GETHOMESTATUS_ENDPOINT, @@ -116,6 +116,9 @@ async def async_update_status(self, home_id: str | None) -> None: ) raw_data = extract_raw_data(await resp.json(), HOME) await self.all_account_homes[h_id].update(raw_data) + num_calls += 1 + + return num_calls async def async_update_events(self, home_id: str) -> None: """Retrieve events from /getevents.""" From 938080a8e6e09c4cddd7b53e25c16d36e99e067c Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 13 Mar 2024 01:06:51 +0100 Subject: [PATCH 19/97] async update for all homes --- src/pyatmo/modules/module.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 9a4365da..3cd747d5 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -598,6 +598,9 @@ def reset_measures(self): self.end_time = None self.historical_data = [] self.sum_energy_elec = 0 + self.sum_energy_elec_peak = 0 + self.sum_energy_elec_off_peak = 0 + def _log_energy_error(self, start_time, end_time, msg=None, body=None): if body is None: @@ -829,10 +832,10 @@ async def async_update_measures( LOG.debug("=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec) if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: LOG.debug( - ">>>>>>>>>> ENERGY GOING DOWN %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f", + ">>>>>>>>>> ENERGY GOING DOWN %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f prev_start: %s, prev_end %s", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, - prev_sum_energy_elec) + prev_sum_energy_elec, datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) return num_calls From 5ad1fe1ae747303566e58be6e00525a2b82775b7 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 13 Mar 2024 22:42:19 +0100 Subject: [PATCH 20/97] async update for all homes --- src/pyatmo/modules/module.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 3cd747d5..c3268556 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -740,6 +740,7 @@ async def async_update_measures( interval_sec = int(values_lot["step_time"]) except: self._log_energy_error(start_time, end_time, msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", body=raw_datas[cur_energy_peak_or_off_peak_mode]) + interval_sec = 2*delta_range #maybe we contineu with default step time? cur_start_time = start_lot_time @@ -755,7 +756,7 @@ async def async_update_measures( srt_beg = cur_start_time - day_origin #now check if srt_beg is in a schedule span of the right type - idx_start = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg) + idx_start = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg - interval_sec//2) if self.home.energy_schedule_vals[idx_start][1] != cur_energy_peak_or_off_peak_mode: @@ -829,8 +830,10 @@ async def async_update_measures( }, ) - LOG.debug("=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec) - if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: + if prev_sum_energy_elec is None: + prev_sum_energy_elec = "NOTHING" + LOG.debug("=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%s", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec) + if prev_sum_energy_elec is not None and prev_sum_energy_elec != "NOTHING" and prev_sum_energy_elec > self.sum_energy_elec: LOG.debug( ">>>>>>>>>> ENERGY GOING DOWN %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f prev_start: %s, prev_end %s", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), From 3ab9ec0524f2bc964c046f4a733a84fc7cb427c1 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 13 Mar 2024 22:48:18 +0100 Subject: [PATCH 21/97] async update for all homes --- src/pyatmo/modules/module.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index c3268556..4bd2bd0b 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -648,7 +648,7 @@ async def async_update_measures( data_points = self.home.energy_endpoints raw_datas = [] - LOG.debug("INFO: doing async_update_measures for %s", self.name) + #LOG.debug("INFO: doing async_update_measures for %s", self.name) for data_point in data_points: @@ -830,15 +830,19 @@ async def async_update_measures( }, ) - if prev_sum_energy_elec is None: - prev_sum_energy_elec = "NOTHING" - LOG.debug("=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%s", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec) - if prev_sum_energy_elec is not None and prev_sum_energy_elec != "NOTHING" and prev_sum_energy_elec > self.sum_energy_elec: + + if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: LOG.debug( ">>>>>>>>>> ENERGY GOING DOWN %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f prev_start: %s, prev_end %s", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec, datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) + else: + LOG.debug( + "=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%s", + self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), + datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, + prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") return num_calls From 1cb8d3e19b244292d9c9650aca1bfc4a02603bb6 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 14 Mar 2024 20:30:25 +0100 Subject: [PATCH 22/97] async update for all homes --- src/pyatmo/account.py | 43 ++++++++++++++++++++++-------------- src/pyatmo/modules/module.py | 16 +++++++++----- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 02c3b9a9..fd566a76 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -88,8 +88,8 @@ def find_from_all_homes(self, home_id): home = self.additional_public_homes.get(home_id) return home - async def async_update_topology(self) -> None: - """Retrieve topology data from /homesdata.""" + async def async_update_topology(self) -> int: + """Retrieve topology data from /homesdata. Returns the number of performed API calls""" resp = await self.auth.async_post_api_request( endpoint=GETHOMESDATA_ENDPOINT, @@ -100,8 +100,10 @@ async def async_update_topology(self) -> None: self.process_topology() + return 1 + async def async_update_status(self, home_id: str | None = None) -> int: - """Retrieve status data from /homestatus.""" + """Retrieve status data from /homestatus. Returns the number of performed API calls""" if home_id is None: self.update_supported_homes(self.support_only_homes) @@ -120,8 +122,8 @@ async def async_update_status(self, home_id: str | None = None) -> int: return num_calls - async def async_update_events(self, home_id: str) -> None: - """Retrieve events from /getevents.""" + async def async_update_events(self, home_id: str) -> int: + """Retrieve events from /getevents. Returns the number of performed API calls""" resp = await self.auth.async_post_api_request( endpoint=GETEVENTS_ENDPOINT, params={"home_id": home_id}, @@ -129,18 +131,23 @@ async def async_update_events(self, home_id: str) -> None: raw_data = extract_raw_data(await resp.json(), HOME) await self.all_account_homes[home_id].update(raw_data) - async def async_update_weather_stations(self) -> None: - """Retrieve status data from /getstationsdata.""" + return 1 + + async def async_update_weather_stations(self) -> int: + """Retrieve status data from /getstationsdata. Returns the number of performed API calls""" params = {"get_favorites": ("true" if self.favorite_stations else "false")} await self._async_update_data( GETSTATIONDATA_ENDPOINT, params=params, ) + return 1 - async def async_update_air_care(self) -> None: - """Retrieve status data from /gethomecoachsdata.""" + async def async_update_air_care(self) -> int: + """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls""" await self._async_update_data(GETHOMECOACHDATA_ENDPOINT) + return 1 + async def async_update_measures( self, home_id: str, @@ -149,15 +156,16 @@ async def async_update_measures( interval: MeasureInterval = MeasureInterval.HOUR, days: int = 7, end_time: int | None = None - ) -> None: - """Retrieve measures data from /getmeasure.""" + ) -> int: + """Retrieve measures data from /getmeasure. Returns the number of performed API calls""" - await getattr(self.find_from_all_homes(home_id).modules[module_id], "async_update_measures")( + num_calls = await getattr(self.find_from_all_homes(home_id).modules[module_id], "async_update_measures")( start_time=start_time, end_time=end_time, interval=interval, days=days, ) + return num_calls def register_public_weather_area( self, @@ -182,8 +190,8 @@ def register_public_weather_area( ) return area_id - async def async_update_public_weather(self, area_id: str) -> None: - """Retrieve status data from /getpublicdata.""" + async def async_update_public_weather(self, area_id: str) -> int: + """Retrieve status data from /getpublicdata. Returns the number of performed API calls""" params = { "lat_ne": self.public_weather_areas[area_id].location.lat_ne, "lon_ne": self.public_weather_areas[area_id].location.lon_ne, @@ -200,6 +208,8 @@ async def async_update_public_weather(self, area_id: str) -> None: area_id=area_id, ) + return 1 + async def _async_update_data( self, endpoint: str, @@ -212,8 +222,8 @@ async def _async_update_data( raw_data = extract_raw_data(await resp.json(), tag) await self.update_devices(raw_data, area_id) - async def async_set_state(self, home_id: str, data: dict[str, Any]) -> None: - """Modify device state by passing JSON specific to the device.""" + async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: + """Modify device state by passing JSON specific to the device. Returns the number of performed API calls""" LOG.debug("Setting state: %s", data) post_params = { @@ -229,6 +239,7 @@ async def async_set_state(self, home_id: str, data: dict[str, Any]) -> None: params=post_params, ) LOG.debug("Response: %s", resp) + return 1 async def update_devices( self, diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 4bd2bd0b..007e99c0 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -611,6 +611,9 @@ def _log_energy_error(self, start_time, end_time, msg=None, body=None): datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), start_time, end_time, body) + def update_measures_num_calls(self): + return 2 + async def async_update_measures( self, start_time: int | None = None, @@ -699,10 +702,10 @@ async def async_update_measures( try: max_interval_sec = max(max_interval_sec, int(values_lot["step_time"])) except: - self._log_energy_error(start_time, end_time, - msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", - body=raw_datas[cur_energy_peak_or_off_peak_mode]) - # maybe we continue with default step time? or do we have an error? + if len(values_lot.get("value", [])) > 1: + self._log_energy_error(start_time, end_time, + msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", + body=raw_datas[cur_energy_peak_or_off_peak_mode]) biggest_day_interval = (max_interval_sec)//(3600*24) + 1 @@ -739,9 +742,10 @@ async def async_update_measures( try: interval_sec = int(values_lot["step_time"]) except: - self._log_energy_error(start_time, end_time, msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", body=raw_datas[cur_energy_peak_or_off_peak_mode]) + if len(values_lot.get("value", [])) > 1: + self._log_energy_error(start_time, end_time, msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", body=raw_datas[cur_energy_peak_or_off_peak_mode]) interval_sec = 2*delta_range - #maybe we contineu with default step time? + cur_start_time = start_lot_time for val_arr in values_lot.get("value",[]): From 2c3e4eaf0e020c9aa3909066bf7527e83a058a19 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 15 Mar 2024 14:43:55 +0100 Subject: [PATCH 23/97] async update for all homes --- src/pyatmo/modules/module.py | 93 ++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 007e99c0..0f09b406 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -594,8 +594,6 @@ def __init__(self, home: Home, module: ModuleT): self.sum_energy_elec_off_peak: int | None = None def reset_measures(self): - self.start_time = None - self.end_time = None self.historical_data = [] self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 @@ -792,61 +790,74 @@ async def async_update_measures( hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) + + + + + self.historical_data = [] prev_sum_energy_elec = self.sum_energy_elec self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - self.end_time = end_time - computed_start = 0 - computed_end = 0 - for cur_start_time, val, cur_energy_peak_or_off_peak_mode in hist_good_vals: + if len(hist_good_vals) == 0: + #nothing has been updated or changed it can nearly be seen as an error, but teh api is answering correctly + #so we probably have to reset to 0 anyway as it means there were no exisitng historical data for this time range + LOG.debug( + "=> NO VALUES energy update %s from: %s to %s, prev_sum=%s", + self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), + prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") + else: + + computed_start = 0 + computed_end = 0 + for cur_start_time, val, cur_energy_peak_or_off_peak_mode in hist_good_vals: - self.sum_energy_elec += val + self.sum_energy_elec += val - if peak_off_peak_mode: - mode = "off_peak" - if cur_energy_peak_or_off_peak_mode == ENERGY_ELEC_PEAK_IDX: - self.sum_energy_elec_peak += val - mode = "peak" + if peak_off_peak_mode: + mode = "off_peak" + if cur_energy_peak_or_off_peak_mode == ENERGY_ELEC_PEAK_IDX: + self.sum_energy_elec_peak += val + mode = "peak" + else: + self.sum_energy_elec_off_peak += val else: - self.sum_energy_elec_off_peak += val - else: - mode = "standard" + mode = "standard" - if computed_start == 0: - computed_start = cur_start_time - delta_range - computed_end = cur_start_time + delta_range + if computed_start == 0: + computed_start = cur_start_time - delta_range + computed_end = cur_start_time + delta_range - self.historical_data.append( - { - "duration": (2*delta_range)//60, - "startTime": f"{datetime.fromtimestamp(cur_start_time - delta_range + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", - "endTime": f"{datetime.fromtimestamp(cur_start_time + delta_range, tz=timezone.utc).isoformat().split('+')[0]}Z", - "Wh": val, - "energyMode": mode, - "startTimeUnix": cur_start_time - delta_range, - "endTimeUnix": cur_start_time + delta_range + self.historical_data.append( + { + "duration": (2*delta_range)//60, + "startTime": f"{datetime.fromtimestamp(cur_start_time - delta_range + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", + "endTime": f"{datetime.fromtimestamp(cur_start_time + delta_range, tz=timezone.utc).isoformat().split('+')[0]}Z", + "Wh": val, + "energyMode": mode, + "startTimeUnix": cur_start_time - delta_range, + "endTimeUnix": cur_start_time + delta_range - }, - ) + }, + ) - if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: - LOG.debug( - ">>>>>>>>>> ENERGY GOING DOWN %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f prev_start: %s, prev_end %s", - self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), - datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, - prev_sum_energy_elec, datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) - else: - LOG.debug( - "=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%s", - self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), - datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, - prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") + if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: + LOG.debug( + ">>>>>>>>>> ENERGY GOING DOWN %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f prev_start: %s, prev_end %s", + self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), + datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, + prev_sum_energy_elec, datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) + else: + LOG.debug( + "=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%s", + self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), + datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, + prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") return num_calls From 2fde12432bd5d3f3b77f82f01c9af2ca7633e0de Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 15 Mar 2024 17:56:09 +0100 Subject: [PATCH 24/97] async update for all homes --- src/pyatmo/modules/module.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 0f09b406..c86c6e17 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -745,7 +745,8 @@ async def async_update_measures( interval_sec = 2*delta_range - cur_start_time = start_lot_time + #align the start on the begining of the segment + cur_start_time = start_lot_time - interval_sec//2 for val_arr in values_lot.get("value",[]): val = val_arr[0] @@ -758,7 +759,7 @@ async def async_update_measures( srt_beg = cur_start_time - day_origin #now check if srt_beg is in a schedule span of the right type - idx_start = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg - interval_sec//2) + idx_start = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg) if self.home.energy_schedule_vals[idx_start][1] != cur_energy_peak_or_off_peak_mode: @@ -782,7 +783,7 @@ async def async_update_measures( start_time_to_get_closer = energy_schedule_vals[idx_start+1][0] diff_t = start_time_to_get_closer - srt_beg - cur_start_time = day_origin + srt_beg + (diff_t//interval_sec + 1)*interval_sec + cur_start_time = day_origin + srt_beg + (diff_t//interval_sec)*interval_sec hist_good_vals.append((cur_start_time, val, cur_energy_peak_or_off_peak_mode)) cur_start_time = cur_start_time + interval_sec @@ -827,20 +828,24 @@ async def async_update_measures( else: mode = "standard" + + c_start = cur_start_time + c_end = cur_start_time + 2*delta_range + if computed_start == 0: - computed_start = cur_start_time - delta_range - computed_end = cur_start_time + delta_range + computed_start = c_start + computed_end = c_end self.historical_data.append( { "duration": (2*delta_range)//60, - "startTime": f"{datetime.fromtimestamp(cur_start_time - delta_range + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", - "endTime": f"{datetime.fromtimestamp(cur_start_time + delta_range, tz=timezone.utc).isoformat().split('+')[0]}Z", + "startTime": f"{datetime.fromtimestamp(c_start + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", + "endTime": f"{datetime.fromtimestamp(c_end, tz=timezone.utc).isoformat().split('+')[0]}Z", "Wh": val, "energyMode": mode, - "startTimeUnix": cur_start_time - delta_range, - "endTimeUnix": cur_start_time + delta_range + "startTimeUnix": c_start, + "endTimeUnix": c_end }, ) From 72508f347b1a97c6f1ddea65ebb1dbae86d3fe12 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 15 Mar 2024 18:21:50 +0100 Subject: [PATCH 25/97] added some tests for historical data --- ...energy_elec$1_98_76_54_32_10_00_00_49.json | 1 + ...energy_elec$2_98_76_54_32_10_00_00_49.json | 1 + src/pyatmo/modules/module.py | 20 ++++----- tests/test_energy.py | 42 ++++++++++++++++++- 4 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_49.json create mode 100644 fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_49.json diff --git a/fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_49.json b/fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_49.json new file mode 100644 index 00000000..0f60f8f8 --- /dev/null +++ b/fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_49.json @@ -0,0 +1 @@ +{"body":[{"beg_time":1710459891,"step_time":1800,"value":[[0],[0]]},{"beg_time":1710465291,"step_time":1800,"value":[[0],[222],[328],[333],[7],[0],[0],[0],[0],[0],[0],[0],[0],[0]]}],"status":"ok","time_exec":0.08218002319335938,"time_server":1710510905} \ No newline at end of file diff --git a/fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_49.json b/fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_49.json new file mode 100644 index 00000000..03784fcd --- /dev/null +++ b/fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_49.json @@ -0,0 +1 @@ +{"body":[{"beg_time":1710465291,"step_time":1800,"value":[[0],[0],[0],[0],[0],[0],[0],[0],[332],[448]]}],"status":"ok","time_exec":0.02698206901550293,"time_server":1710511044} \ No newline at end of file diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index c86c6e17..c57acdd3 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -757,14 +757,15 @@ async def async_update_measures( #offset from start of the day day_origin = int(datetime(d_srt.year, d_srt.month, d_srt.day).timestamp()) srt_beg = cur_start_time - day_origin + srt_mid = srt_beg + interval_sec//2 #now check if srt_beg is in a schedule span of the right type - idx_start = self._get_proper_in_schedule_index(energy_schedule_vals, srt_beg) + idx_limit = self._get_proper_in_schedule_index(energy_schedule_vals, srt_mid) - if self.home.energy_schedule_vals[idx_start][1] != cur_energy_peak_or_off_peak_mode: + if self.home.energy_schedule_vals[idx_limit][1] != cur_energy_peak_or_off_peak_mode: #we are NOT in a proper schedule time for this time span ... jump to the next one... meaning it is the next day! - if idx_start == len(energy_schedule_vals) - 1: + if idx_limit == len(energy_schedule_vals) - 1: #should never append with the performed day extension above self._log_energy_error(start_time, end_time, msg=f"bad idx missing {data_points[cur_energy_peak_or_off_peak_mode]}", @@ -773,7 +774,7 @@ async def async_update_measures( return 0 else: #by construction of the energy schedule the next one should be of opposite mode - if energy_schedule_vals[idx_start + 1][1] != cur_energy_peak_or_off_peak_mode: + if energy_schedule_vals[idx_limit + 1][1] != cur_energy_peak_or_off_peak_mode: self._log_energy_error(start_time, end_time, msg=f"bad schedule {data_points[cur_energy_peak_or_off_peak_mode]}", body=raw_datas[cur_energy_peak_or_off_peak_mode]) @@ -781,9 +782,9 @@ async def async_update_measures( - start_time_to_get_closer = energy_schedule_vals[idx_start+1][0] - diff_t = start_time_to_get_closer - srt_beg - cur_start_time = day_origin + srt_beg + (diff_t//interval_sec)*interval_sec + start_time_to_get_closer = energy_schedule_vals[idx_limit+1][0] + diff_t = start_time_to_get_closer - srt_mid + cur_start_time = day_origin + srt_beg + (diff_t//interval_sec + 1)*interval_sec hist_good_vals.append((cur_start_time, val, cur_energy_peak_or_off_peak_mode)) cur_start_time = cur_start_time + interval_sec @@ -791,11 +792,6 @@ async def async_update_measures( hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) - - - - - self.historical_data = [] prev_sum_energy_elec = self.sum_energy_elec diff --git a/tests/test_energy.py b/tests/test_energy.py index 186e41c5..8ffe6a96 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -53,8 +53,12 @@ async def test_historical_data_retrieval_multi(async_account_multi): module = home.modules[module_id] assert module.device_type == DeviceType.NLC - strt = 1709421000 + strt = 1709421000 end_time = 1709679599 + + strt = int(dt.datetime.fromisoformat("2024-03-03 00:10:00").timestamp()) + end_time = int(dt.datetime.fromisoformat("2024-03-05 23:59:59").timestamp()) + await async_account_multi.async_update_measures(home_id=home_id, module_id=module_id, interval=MeasureInterval.HALF_HOUR, @@ -72,3 +76,39 @@ async def test_historical_data_retrieval_multi(async_account_multi): assert module.sum_energy_elec_peak == 31282 + + + + +async def test_historical_data_retrieval_multi_2(async_account_multi): + """Test retrieval of historical measurements.""" + home_id = "aaaaaaaaaaabbbbbbbbbbccc" + + home = async_account_multi.homes[home_id] + + module_id = "98:76:54:32:10:00:00:49" + assert module_id in home.modules + module = home.modules[module_id] + assert module.device_type == DeviceType.NLC + + + + + strt = int(dt.datetime.fromisoformat("2024-03-15 00:29:51").timestamp()) + end = int(dt.datetime.fromisoformat("2024-03-15 13:45:24").timestamp()) + + await async_account_multi.async_update_measures(home_id=home_id, + module_id=module_id, + interval=MeasureInterval.HALF_HOUR, + start_time=strt, + end_time=end + ) + + + assert module.historical_data[0] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-14T23:59:51Z', 'endTimeUnix': 1710460791, 'energyMode': 'peak', 'startTime': '2024-03-14T23:29:52Z', 'startTimeUnix': 1710458991} + assert module.historical_data[-1] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-15T12:59:51Z', 'endTimeUnix': 1710507591, 'energyMode': 'peak', 'startTime': '2024-03-15T12:29:52Z', 'startTimeUnix': 1710505791} + assert len(module.historical_data) == 26 + + assert module.sum_energy_elec == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak + assert module.sum_energy_elec_off_peak == 780 + assert module.sum_energy_elec_peak == 890 From e0d6a1ffd10dbbc6f9cf134f6849d2a11eb9d99e Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 17 Mar 2024 13:22:31 +0100 Subject: [PATCH 26/97] added some tests for historical data --- src/pyatmo/account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index fd566a76..377e4313 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -54,7 +54,6 @@ def __repr__(self) -> str: def update_supported_homes(self, support_only_homes: list | None = None): - self.support_only_homes = support_only_homes if support_only_homes is None or len(support_only_homes) == 0: self.homes = copy.copy(self.all_account_homes) else: @@ -65,9 +64,10 @@ def update_supported_homes(self, support_only_homes: list | None = None): self.homes[h_id] = h if len(self.homes) == 0: - self.support_only_homes = None self.homes = copy.copy(self.all_account_homes) + self.support_only_homes = [h_id for h_id in self.homes] + self.homes.update(self.additional_public_homes) From 024344ac572f7592e76a8d0b1ab242a8acf8d21b Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 17 Mar 2024 22:02:53 +0100 Subject: [PATCH 27/97] added a way to get the data sum --- src/pyatmo/account.py | 9 +++++++++ src/pyatmo/modules/module.py | 4 ++-- tests/test_energy.py | 5 +++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 377e4313..455f4db4 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -167,6 +167,15 @@ async def async_update_measures( ) return num_calls + def get_current_energy_sum(self): + sum = 0 + for h_id, home in self.homes.items(): + for m_id, module in home.modules.items(): + v = getattr(module, "sum_energy_elec", None) + if v is not None: + sum += v + return sum + def register_public_weather_area( self, lat_ne: str, diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index c57acdd3..9b876e96 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -786,7 +786,7 @@ async def async_update_measures( diff_t = start_time_to_get_closer - srt_mid cur_start_time = day_origin + srt_beg + (diff_t//interval_sec + 1)*interval_sec - hist_good_vals.append((cur_start_time, val, cur_energy_peak_or_off_peak_mode)) + hist_good_vals.append((cur_start_time, int(val), cur_energy_peak_or_off_peak_mode)) cur_start_time = cur_start_time + interval_sec @@ -855,7 +855,7 @@ async def async_update_measures( prev_sum_energy_elec, datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) else: LOG.debug( - "=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%s", + "=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%s prev_sum=%s", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") diff --git a/tests/test_energy.py b/tests/test_energy.py index 8ffe6a96..49c81d7a 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -112,3 +112,8 @@ async def test_historical_data_retrieval_multi_2(async_account_multi): assert module.sum_energy_elec == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak assert module.sum_energy_elec_off_peak == 780 assert module.sum_energy_elec_peak == 890 + + + sum = async_account_multi.get_current_energy_sum() + + assert module.sum_energy_elec == sum From 252280ed124f27cd6751adeb90ff1bacc413c822 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 18 Mar 2024 19:01:46 +0100 Subject: [PATCH 28/97] added throttling exception --- src/pyatmo/__init__.py | 3 ++- src/pyatmo/auth.py | 16 ++++++++-------- src/pyatmo/exceptions.py | 5 +++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/__init__.py b/src/pyatmo/__init__.py index 2bba15ea..22e2b6e2 100644 --- a/src/pyatmo/__init__.py +++ b/src/pyatmo/__init__.py @@ -2,7 +2,7 @@ from pyatmo import const, modules from pyatmo.account import AsyncAccount from pyatmo.auth import AbstractAsyncAuth -from pyatmo.exceptions import ApiError, InvalidHome, InvalidRoom, NoDevice, NoSchedule +from pyatmo.exceptions import ApiError, ApiErrorThrottling, InvalidHome, InvalidRoom, NoDevice, NoSchedule from pyatmo.home import Home from pyatmo.modules import Module from pyatmo.modules.device_types import DeviceType @@ -11,6 +11,7 @@ __all__ = [ "AbstractAsyncAuth", "ApiError", + "ApiErrorThrottling", "AsyncAccount", "InvalidHome", "InvalidRoom", diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 970e52ba..3189cde8 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -16,7 +16,7 @@ WEBHOOK_URL_ADD_ENDPOINT, WEBHOOK_URL_DROP_ENDPOINT, ) -from pyatmo.exceptions import ApiError +from pyatmo.exceptions import ApiError, ApiErrorThrottling LOG = logging.getLogger(__name__) @@ -145,13 +145,13 @@ async def handle_error_response(self, resp, resp_status, url): """Handle error response.""" try: resp_json = await resp.json() - raise ApiError( - f"{resp_status} - " - f"{ERRORS.get(resp_status, '')} - " - f"{resp_json['error']['message']} " - f"({resp_json['error']['code']}) " - f"when accessing '{url}'", - ) + + message = f"{resp_status} - {ERRORS.get(resp_status, '')} - {resp_json['error']['message']} ({resp_json['error']['code']}) when accessing '{url}'" + + if resp_status == 403 and resp_json['error']['code'] == 26: + raise ApiErrorThrottling(message, ) + else: + raise ApiError(message,) except (JSONDecodeError, ContentTypeError) as exc: raise ApiError( diff --git a/src/pyatmo/exceptions.py b/src/pyatmo/exceptions.py index 4ed5f120..1833e5e4 100644 --- a/src/pyatmo/exceptions.py +++ b/src/pyatmo/exceptions.py @@ -36,6 +36,11 @@ class ApiError(Exception): pass +class ApiErrorThrottling(ApiError): + """Raised when an API error is encountered.""" + + pass + class InvalidState(Exception): """Raised when an invalid state is encountered.""" From 9b4c539a4a8fcebf7d6310c657de56ccfceedecc Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 19 Mar 2024 20:52:24 +0100 Subject: [PATCH 29/97] fix for num calls estimation --- src/pyatmo/modules/module.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 9b876e96..88a44953 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -610,7 +610,10 @@ def _log_energy_error(self, start_time, end_time, msg=None, body=None): datetime.fromtimestamp(end_time), start_time, end_time, body) def update_measures_num_calls(self): - return 2 + try: + return len(self.home.energy_endpoints) + except: + return 1 async def async_update_measures( self, From c88c2f3265f5f9b1e2e7b6ad652c1174a16d21ac Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sat, 23 Mar 2024 08:54:39 +0100 Subject: [PATCH 30/97] fix for num calls estimation --- src/pyatmo/auth.py | 51 +++++++++++++++++++++++++++++++++++++++++++++ src/pyatmo/const.py | 1 + 2 files changed, 52 insertions(+) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 3189cde8..ad881df8 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -88,6 +88,42 @@ async def async_post_api_request( timeout=timeout, ) + async def async_get_api_request( + self, + endpoint: str, + base_url: str | None = None, + params: dict[str, Any] | None = None, + timeout: int = 5, + ) -> ClientResponse: + """Wrap async post requests.""" + + return await self.async_get_request( + url=(base_url or self.base_url) + endpoint, + params=params, + timeout=timeout, + ) + + async def async_get_request( + self, + url: str, + params: dict[str, Any] | None = None, + timeout: int = 5, + ) -> ClientResponse: + """Wrap async post requests.""" + + access_token = await self.get_access_token() + headers = {AUTHORIZATION_HEADER: f"Bearer {access_token}"} + + req_args = self.prepare_request_get_arguments(params) + + async with self.websession.get( + url, + **req_args, + headers=headers, + timeout=timeout, + ) as resp: + return await self.process_response(resp, url) + async def async_post_request( self, url: str, @@ -130,6 +166,21 @@ def prepare_request_arguments(self, params): return req_args + def prepare_request_get_arguments(self, params): + return params + """Prepare request arguments.""" + req_args = {"data": params if params is not None else {}} + + if "params" in req_args["data"]: + req_args["params"] = req_args["data"]["params"] + req_args["data"].pop("params") + + if "json" in req_args["data"]: + req_args["json"] = req_args["data"]["json"] + req_args.pop("data") + + return req_args + async def process_response(self, resp, url): """Process response.""" resp_status = resp.status diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index cbaa8677..2fd9d416 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -45,6 +45,7 @@ GETHOMECOACHDATA_ENDPOINT = "api/gethomecoachsdata" GETMEASURE_ENDPOINT = "api/getmeasure" +GETHOMEMEASURE_ENDPOINT = "api/gethomemeasure" GETSTATIONDATA_ENDPOINT = "api/getstationsdata" GETPUBLIC_DATA_ENDPOINT = "api/getpublicdata" From d593bfa7c4fdb5b5a0b1f658d2c1bbcea5bd946e Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 25 Mar 2024 21:04:40 +0100 Subject: [PATCH 31/97] changed type for energy --- src/pyatmo/const.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index 2fd9d416..0a9c61e3 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -121,13 +121,13 @@ class MeasureType(Enum): SUM_BOILER_ON = "sum_boiler_on" SUM_BOILER_OFF = "sum_boiler_off" SUM_ENERGY_ELEC = "sum_energy_elec" - SUM_ENERGY_ELEC_BASIC = "sum_energy_elec$0" - SUM_ENERGY_ELEC_PEAK = "sum_energy_elec$1" - SUM_ENERGY_ELEC_OFF_PEAK = "sum_energy_elec$2" + SUM_ENERGY_ELEC_BASIC = "sum_energy_buy_from_grid$0" + SUM_ENERGY_ELEC_PEAK = "sum_energy_buy_from_grid$1" + SUM_ENERGY_ELEC_OFF_PEAK = "sum_energy_buy_from_grid$2" SUM_ENERGY_PRICE = "sum_energy_price" - SUM_ENERGY_PRICE_BASIC = "sum_energy_price$0" - SUM_ENERGY_PRICE_PEAK = "sum_energy_price$1" - SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_price$2" + SUM_ENERGY_PRICE_BASIC = "sum_energy_buy_from_grid_price$0" + SUM_ENERGY_PRICE_PEAK = "sum_energy_buy_from_grid_price$1" + SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_buy_from_grid_price$2" class MeasureInterval(Enum): From 645b3d73a98006a5a66c62f21035f25284539c8e Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 25 Mar 2024 22:13:23 +0100 Subject: [PATCH 32/97] changed type for energy --- src/pyatmo/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index 0a9c61e3..93d780b3 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -120,11 +120,11 @@ class MeasureType(Enum): BOILEROFF = "boileroff" SUM_BOILER_ON = "sum_boiler_on" SUM_BOILER_OFF = "sum_boiler_off" - SUM_ENERGY_ELEC = "sum_energy_elec" + SUM_ENERGY_ELEC = "sum_energy_buy_from_grid" SUM_ENERGY_ELEC_BASIC = "sum_energy_buy_from_grid$0" SUM_ENERGY_ELEC_PEAK = "sum_energy_buy_from_grid$1" SUM_ENERGY_ELEC_OFF_PEAK = "sum_energy_buy_from_grid$2" - SUM_ENERGY_PRICE = "sum_energy_price" + SUM_ENERGY_PRICE = "sum_energy_buy_from_grid_price" SUM_ENERGY_PRICE_BASIC = "sum_energy_buy_from_grid_price$0" SUM_ENERGY_PRICE_PEAK = "sum_energy_buy_from_grid_price$1" SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_buy_from_grid_price$2" From 9c6e16cafeeeb2ba3c4187a280c23c171de7d557 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 26 Mar 2024 15:01:50 +0100 Subject: [PATCH 33/97] Added historical retrival for other state, begining only for power (to be used to refine homeassistant energy handling --- ..._from_grid$0_12_34_56_00_00_a1_4c_da.json} | 0 ..._from_grid$1_98_76_54_32_10_00_00_49.json} | 0 ..._from_grid$1_98_76_54_32_10_00_00_73.json} | 0 ..._from_grid$2_98_76_54_32_10_00_00_49.json} | 0 ..._from_grid$2_98_76_54_32_10_00_00_73.json} | 0 src/pyatmo/modules/base_class.py | 56 +++++++++++++++++++ src/pyatmo/modules/module.py | 1 + 7 files changed, 57 insertions(+) rename fixtures/{getmeasure_sum_energy_elec$0_12_34_56_00_00_a1_4c_da.json => getmeasure_sum_energy_buy_from_grid$0_12_34_56_00_00_a1_4c_da.json} (100%) rename fixtures/{getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_49.json => getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_49.json} (100%) rename fixtures/{getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_73.json => getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_73.json} (100%) rename fixtures/{getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_49.json => getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_49.json} (100%) rename fixtures/{getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_73.json => getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_73.json} (100%) diff --git a/fixtures/getmeasure_sum_energy_elec$0_12_34_56_00_00_a1_4c_da.json b/fixtures/getmeasure_sum_energy_buy_from_grid$0_12_34_56_00_00_a1_4c_da.json similarity index 100% rename from fixtures/getmeasure_sum_energy_elec$0_12_34_56_00_00_a1_4c_da.json rename to fixtures/getmeasure_sum_energy_buy_from_grid$0_12_34_56_00_00_a1_4c_da.json diff --git a/fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_49.json b/fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_49.json similarity index 100% rename from fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_49.json rename to fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_49.json diff --git a/fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_73.json b/fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_73.json similarity index 100% rename from fixtures/getmeasure_sum_energy_elec$1_98_76_54_32_10_00_00_73.json rename to fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_73.json diff --git a/fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_49.json b/fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_49.json similarity index 100% rename from fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_49.json rename to fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_49.json diff --git a/fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_73.json b/fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_73.json similarity index 100% rename from fixtures/getmeasure_sum_energy_elec$2_98_76_54_32_10_00_00_73.json rename to fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_73.json diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 41701547..c3ae1be0 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -1,10 +1,12 @@ """Base class for Netatmo entities.""" from __future__ import annotations +import bisect from abc import ABC from collections.abc import Iterable from dataclasses import dataclass import logging +from operator import itemgetter from typing import TYPE_CHECKING, Any from pyatmo.const import RawData @@ -14,6 +16,8 @@ from pyatmo.event import EventTypes from pyatmo.home import Home +from time import time + LOG = logging.getLogger(__name__) @@ -49,7 +53,10 @@ class EntityBase: entity_id: str home: Home bridge: str | None + history_features: set[str] = set() + +MAX_HISTORY_TIME_S = 24*2*3600 #2 days of dynamic historical data stored class NetatmoBase(EntityBase, ABC): """Base class for Netatmo entities.""" @@ -59,6 +66,7 @@ def __init__(self, raw_data: RawData) -> None: self.entity_id = raw_data["id"] self.name = raw_data.get("name", f"Unknown {self.entity_id}") + self.history_features_values: dict[str,[int,int]] | {} = {} def update_topology(self, raw_data: RawData) -> None: """Update topology.""" @@ -80,6 +88,54 @@ def _update_attributes(self, raw_data: RawData) -> None: for key, val in self.__dict__.items() } + now = int(time()) + for hist_feature in self.history_features: + if hist_feature in self.__dict__: + hist_f = self.history_features_values.get(hist_feature, None) + if hist_f is None: + hist_f = [] + self.history_features_values[hist_feature] = hist_f + val = getattr(self, hist_feature) + if not hist_f or hist_f[-1][0] <= now: + hist_f.append((now, val)) + else: + i = bisect.bisect_left(hist_f, now, key=itemgetter(0)) + + if i < len(hist_f): + if hist_f[i][0] == now: + hist_f[i] = (now, val) + i = None + + if i is not None: + hist_f.insert(i, (now,val)) + + #keep timing history to a maximum representative time + while len(hist_f) > 0 and now - hist_f[0][0] > MAX_HISTORY_TIME_S: + hist_f.pop(0) + + LOG.debug(">>>>>> Features History : %s : %s", hist_feature, hist_f) + + + def get_history_data(self, type:str, from_ts: int, to_ts: int | None=None): + + hist_f = self.history_features_values.get(type, []) + + if not hist_f: + return [] + + in_s = bisect.bisect_left(hist_f, from_ts, key=itemgetter(0)) + + if to_ts is None: + out_s = len(hist_f) + else: + out_s = bisect.bisect_right(hist_f, from_ts, key=itemgetter(0)) + + return self.history_features_values[in_s, out_s] + + + + + @dataclass class Location: diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 88a44953..97b1bf55 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -301,6 +301,7 @@ def __init__(self, home: Home, module: ModuleT): super().__init__(home, module) # type: ignore # mypy issue 4335 self.power: int | None = None + self.history_features.add("power") class EventMixin(EntityBase): From 936aeb954d2cf25aaf17d79d6ee2ef38c2ef5b6a Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 26 Mar 2024 15:11:37 +0100 Subject: [PATCH 34/97] Added historical retrival for other state, begining only for power (to be used to refine homeassistant energy handling --- src/pyatmo/modules/module.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 97b1bf55..88400b00 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -596,6 +596,8 @@ def __init__(self, home: Home, module: ModuleT): def reset_measures(self): self.historical_data = [] + self.last_computed_start = None + self.last_computed_end = None self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 @@ -802,6 +804,8 @@ async def async_update_measures( self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 + self.last_computed_start = None + self.last_computed_end = None if len(hist_good_vals) == 0: #nothing has been updated or changed it can nearly be seen as an error, but teh api is answering correctly @@ -864,6 +868,9 @@ async def async_update_measures( datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") + self.last_computed_start = computed_start + self.last_computed_end = computed_end + return num_calls def _get_proper_in_schedule_index(self, energy_schedule_vals, srt_beg): From 7f626ed20708e5b1af300234716e595cb29c3b3e Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 26 Mar 2024 15:41:22 +0100 Subject: [PATCH 35/97] Added historical retrival for other state, begining only for power (to be used to refine homeassistant energy handling --- src/pyatmo/modules/module.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 88400b00..59a3e09a 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -593,8 +593,12 @@ def __init__(self, home: Home, module: ModuleT): self.sum_energy_elec: int | None = None self.sum_energy_elec_peak: int | None = None self.sum_energy_elec_off_peak: int | None = None + self.last_computed_start: int | None = None + self.last_computed_end: int | None = None + self.in_reset: bool| False = False def reset_measures(self): + self.in_reset = True self.historical_data = [] self.last_computed_start = None self.last_computed_end = None @@ -806,6 +810,7 @@ async def async_update_measures( self.sum_energy_elec_off_peak = 0 self.last_computed_start = None self.last_computed_end = None + self.in_reset = False if len(hist_good_vals) == 0: #nothing has been updated or changed it can nearly be seen as an error, but teh api is answering correctly From 843102079920be8df8bcb7647db3b1424c2a4061 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 26 Mar 2024 15:46:20 +0100 Subject: [PATCH 36/97] Added historical retrival for other state, begining only for power (to be used to refine homeassistant energy handling --- src/pyatmo/modules/module.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 59a3e09a..6cdf000b 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -808,13 +808,14 @@ async def async_update_measures( self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - self.last_computed_start = None - self.last_computed_end = None + self.last_computed_start = start_time + self.last_computed_end = start_time # no data at all : we know nothing for the end : best guess .. it is the start self.in_reset = False if len(hist_good_vals) == 0: - #nothing has been updated or changed it can nearly be seen as an error, but teh api is answering correctly + #nothing has been updated or changed it can nearly be seen as an error, but the api is answering correctly #so we probably have to reset to 0 anyway as it means there were no exisitng historical data for this time range + LOG.debug( "=> NO VALUES energy update %s from: %s to %s, prev_sum=%s", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), From c3b55ef1f7c1c2434b4dfbec7018b424f33a674e Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 26 Mar 2024 17:21:59 +0100 Subject: [PATCH 37/97] Added historical retrival for other state, begining only for power (to be used to refine homeassistant energy handling --- src/pyatmo/modules/base_class.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index c3ae1be0..954cd3b8 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -96,6 +96,8 @@ def _update_attributes(self, raw_data: RawData) -> None: hist_f = [] self.history_features_values[hist_feature] = hist_f val = getattr(self, hist_feature) + if val is None: + continue if not hist_f or hist_f[-1][0] <= now: hist_f.append((now, val)) else: From 001f92eb40233469a74e5143956be260652d647e Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 26 Mar 2024 18:35:35 +0100 Subject: [PATCH 38/97] Added historical retrival for other state, begining only for power (to be used to refine homeassistant energy handling --- src/pyatmo/modules/base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 954cd3b8..e9d01213 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -132,7 +132,7 @@ def get_history_data(self, type:str, from_ts: int, to_ts: int | None=None): else: out_s = bisect.bisect_right(hist_f, from_ts, key=itemgetter(0)) - return self.history_features_values[in_s, out_s] + return hist_f[in_s:out_s] From 6170e9f5b11483def2de720d752c03631daa7af7 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 26 Mar 2024 18:43:19 +0100 Subject: [PATCH 39/97] Added historical retrival for other state, begining only for power (to be used to refine homeassistant energy handling --- src/pyatmo/modules/base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index e9d01213..293b8a92 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -115,7 +115,7 @@ def _update_attributes(self, raw_data: RawData) -> None: while len(hist_f) > 0 and now - hist_f[0][0] > MAX_HISTORY_TIME_S: hist_f.pop(0) - LOG.debug(">>>>>> Features History : %s : %s", hist_feature, hist_f) + #LOG.debug(">>>>>> Features History : %s : %s", hist_feature, hist_f) def get_history_data(self, type:str, from_ts: int, to_ts: int | None=None): From acc73317d47fcb86ea7f84b97142d0a797e23790 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 27 Mar 2024 00:07:43 +0100 Subject: [PATCH 40/97] Added historical retrival for other state, begining only for power (to be used to refine homeassistant energy handling --- src/pyatmo/modules/base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 2d9611c2..4ae7fa79 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -131,7 +131,7 @@ def get_history_data(self, type:str, from_ts: int, to_ts: int | None=None): if to_ts is None: out_s = len(hist_f) else: - out_s = bisect.bisect_right(hist_f, from_ts, key=itemgetter(0)) + out_s = bisect.bisect_right(hist_f, to_ts, key=itemgetter(0)) return hist_f[in_s:out_s] From 59f6cc92d682dc12c8eedfc7885686a3a6b067a8 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 27 Mar 2024 12:23:46 +0100 Subject: [PATCH 41/97] all tests pass --- src/pyatmo/modules/base_class.py | 13 ++++++++----- src/pyatmo/modules/module.py | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 4ae7fa79..7ca98378 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -54,7 +54,8 @@ class EntityBase: entity_id: str home: Home bridge: str | None - history_features: set[str] = set() + history_features: set[str] + history_features_values: dict[str, [int, int]] | {} MAX_HISTORY_TIME_S = 24*2*3600 #2 days of dynamic historical data stored @@ -67,7 +68,9 @@ def __init__(self, raw_data: RawData) -> None: self.entity_id = raw_data["id"] self.name = raw_data.get("name", f"Unknown {self.entity_id}") - self.history_features_values: dict[str,[int,int]] | {} = {} + self.history_features_values = {} + self.history_features = set() + def update_topology(self, raw_data: RawData) -> None: """Update topology.""" @@ -100,17 +103,17 @@ def _update_attributes(self, raw_data: RawData) -> None: if val is None: continue if not hist_f or hist_f[-1][0] <= now: - hist_f.append((now, val)) + hist_f.append((now, val, self.entity_id)) else: i = bisect.bisect_left(hist_f, now, key=itemgetter(0)) if i < len(hist_f): if hist_f[i][0] == now: - hist_f[i] = (now, val) + hist_f[i] = (now, val, self.entity_id) i = None if i is not None: - hist_f.insert(i, (now,val)) + hist_f.insert(i, (now,val, self.entity_id)) #keep timing history to a maximum representative time while len(hist_f) > 0 and now - hist_f[0][0] > MAX_HISTORY_TIME_S: diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 05efb28a..57f9acac 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -46,6 +46,8 @@ "device_category", "device_type", "features", + "history_features", + "history_features_values" } From 7b9b6dc49a8b89b4d43c0dd2700e2f5c7212573a Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 27 Mar 2024 23:32:17 +0100 Subject: [PATCH 42/97] added energy sum --- src/pyatmo/account.py | 25 +++++++++++++++++++++---- tests/test_energy.py | 6 ++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index b475f1d5..f1154bb0 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -22,6 +22,7 @@ from pyatmo.helpers import extract_raw_data from pyatmo.home import Home from pyatmo.modules.module import Module +from pyatmo.modules.module import EnergyHistoryMixin if TYPE_CHECKING: from pyatmo.auth import AbstractAsyncAuth @@ -168,13 +169,29 @@ async def async_update_measures( ) return num_calls - def get_current_energy_sum(self): + def get_current_energy_sum(self, excluded_modules:set[str] | None = None): sum = 0 + is_in_reset = False + + if excluded_modules is None: + excluded_modules = set() + for h_id, home in self.homes.items(): + if is_in_reset: + break for m_id, module in home.modules.items(): - v = getattr(module, "sum_energy_elec", None) - if v is not None: - sum += v + if m_id in excluded_modules: + continue + if isinstance(module, EnergyHistoryMixin): + if module.in_reset: + is_in_reset = True + break + v = module.sum_energy_elec + if v is not None: + sum += v + if is_in_reset: + return 0 + return sum def register_public_weather_area( diff --git a/tests/test_energy.py b/tests/test_energy.py index 49c81d7a..387a259e 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -7,6 +7,7 @@ import datetime as dt from pyatmo.const import MeasureInterval +from pyatmo.modules.module import EnergyHistoryMixin # pylint: disable=F6401 @@ -66,6 +67,7 @@ async def test_historical_data_retrieval_multi(async_account_multi): end_time=end_time ) + assert isinstance(module, EnergyHistoryMixin) assert module.historical_data[0] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-02T23:40:00Z', 'endTimeUnix': 1709422800, 'energyMode': 'peak', 'startTime': '2024-03-02T23:10:01Z', 'startTimeUnix': 1709421000} assert module.historical_data[-1] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-05T23:10:00Z', 'endTimeUnix': 1709680200, 'energyMode': 'peak', 'startTime': '2024-03-05T22:40:01Z', 'startTimeUnix': 1709678400} @@ -75,6 +77,10 @@ async def test_historical_data_retrieval_multi(async_account_multi): assert module.sum_energy_elec_off_peak == 11219 assert module.sum_energy_elec_peak == 31282 + assert module.sum_energy_elec == async_account_multi.get_current_energy_sum() + assert async_account_multi.get_current_energy_sum(excluded_modules={module_id}) == 0 + + From 89c7309fc9699c35ae6accfabd43be08289ea43c Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 27 Mar 2024 23:55:09 +0100 Subject: [PATCH 43/97] added energy sum --- src/pyatmo/account.py | 10 ++++++--- src/pyatmo/modules/module.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index f1154bb0..ebf98b71 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -169,7 +169,7 @@ async def async_update_measures( ) return num_calls - def get_current_energy_sum(self, excluded_modules:set[str] | None = None): + def get_current_energy_sum(self, power_adapted: bool = True, to_ts: int | float | None =None, excluded_modules:set[str] | None = None): sum = 0 is_in_reset = False @@ -186,9 +186,13 @@ def get_current_energy_sum(self, excluded_modules:set[str] | None = None): if module.in_reset: is_in_reset = True break - v = module.sum_energy_elec + if power_adapted: + v, delta_energy = module.get_sum_energy_elec_power_adapted(to_ts=to_ts) + else: + delta_energy = 0 + v = module.sum_energy_elec if v is not None: - sum += v + sum += v + delta_energy if is_in_reset: return 0 diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 57f9acac..e388ad0a 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -22,6 +22,7 @@ import bisect from operator import itemgetter +from time import time LOG = logging.getLogger(__name__) @@ -609,6 +610,45 @@ def reset_measures(self): self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 + + def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None): + + v = self.sum_energy_elec + + if v is None: + return None, 0 + + delta_energy = 0 + + if self.in_reset is False: + + if to_ts is None: + to_ts = time() + + from_ts = self.last_computed_end + + if from_ts is not None and from_ts < to_ts and isinstance(self, PowerMixin) and isinstance(self, NetatmoBase): + + power_data = self.get_history_data("power", from_ts=from_ts, to_ts=to_ts) + + if len(power_data) > 1: + + #compute a rieman sum, as best as possible , trapezoidal, taking pessimistic asumption as we don't want to artifically go up the previous one (except in rare exceptions like reset, 0 , etc) + + for i in range(len(power_data) - 1): + + dt_h = float(power_data[i+1][0] - power_data[i][0])/3600.0 + + dP_W = abs(float(power_data[i+1][1] - power_data[i][1])) + + dNrj_Wh = dt_h*( min(power_data[i+1][1], power_data[i][1]) + 0.5*dP_W) + + delta_energy += dNrj_Wh + + + return v, delta_energy + + def _log_energy_error(self, start_time, end_time, msg=None, body=None): if body is None: From 238ccea2eff15158ba1bf65361a2751f4c214232 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 28 Mar 2024 07:39:14 +0100 Subject: [PATCH 44/97] added energy sum --- src/pyatmo/account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index ebf98b71..cd5b912a 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -194,9 +194,9 @@ def get_current_energy_sum(self, power_adapted: bool = True, to_ts: int | float if v is not None: sum += v + delta_energy if is_in_reset: - return 0 + return 0, is_in_reset - return sum + return sum, is_in_reset def register_public_weather_area( self, From abf56807052b7574340dc27d519701398cff1170 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 28 Mar 2024 11:39:47 +0100 Subject: [PATCH 45/97] added energy sum --- src/pyatmo/account.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index cd5b912a..5c43a13b 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -193,6 +193,8 @@ def get_current_energy_sum(self, power_adapted: bool = True, to_ts: int | float v = module.sum_energy_elec if v is not None: sum += v + delta_energy + else: + return None, False if is_in_reset: return 0, is_in_reset From 666297b7edcf3e7c7bcd853152c96c4d8afae1fb Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 29 Mar 2024 16:07:08 +0100 Subject: [PATCH 46/97] PEP cleaning to prepare PR --- src/pyatmo/account.py | 21 ++-- src/pyatmo/auth.py | 15 +-- src/pyatmo/const.py | 14 ++- src/pyatmo/exceptions.py | 1 + src/pyatmo/helpers.py | 1 - src/pyatmo/home.py | 42 +++---- src/pyatmo/modules/base_class.py | 21 ++-- src/pyatmo/modules/module.py | 189 +++++++++++++++---------------- src/pyatmo/schedule.py | 37 +++--- tests/test_energy.py | 10 +- 10 files changed, 166 insertions(+), 185 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 5c43a13b..89bfa765 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -33,7 +33,10 @@ class AsyncAccount: """Async class of a Netatmo account.""" - def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True, support_only_homes: list | None = None) -> None: + def __init__(self, + auth: AbstractAsyncAuth, + favorite_stations: bool = True, + support_only_homes: list | None = None) -> None: """Initialize the Netatmo account.""" self.auth: AbstractAsyncAuth = auth @@ -72,7 +75,6 @@ def update_supported_homes(self, support_only_homes: list | None = None): self.homes.update(self.additional_public_homes) - def process_topology(self) -> None: """Process topology information from /homesdata.""" @@ -169,8 +171,13 @@ async def async_update_measures( ) return num_calls - def get_current_energy_sum(self, power_adapted: bool = True, to_ts: int | float | None =None, excluded_modules:set[str] | None = None): - sum = 0 + def get_current_energy_sum(self, + power_adapted: bool = True, + to_ts: int | float | None = None, + excluded_modules: set[str] | None = None, + ok_if_none: bool = False): + + energy_sum = 0 is_in_reset = False if excluded_modules is None: @@ -192,13 +199,13 @@ def get_current_energy_sum(self, power_adapted: bool = True, to_ts: int | float delta_energy = 0 v = module.sum_energy_elec if v is not None: - sum += v + delta_energy - else: + energy_sum += v + delta_energy + elif ok_if_none is False: return None, False if is_in_reset: return 0, is_in_reset - return sum, is_in_reset + return energy_sum, is_in_reset def register_public_weather_area( self, diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 8e0b59d0..4e0052ed 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -169,18 +169,6 @@ def prepare_request_arguments(self, params): def prepare_request_get_arguments(self, params): return params - """Prepare request arguments.""" - req_args = {"data": params if params is not None else {}} - - if "params" in req_args["data"]: - req_args["params"] = req_args["data"]["params"] - req_args["data"].pop("params") - - if "json" in req_args["data"]: - req_args["json"] = req_args["data"]["json"] - req_args.pop("data") - - return req_args async def process_response(self, resp, url): """Process response.""" @@ -198,7 +186,8 @@ async def handle_error_response(self, resp, resp_status, url): try: resp_json = await resp.json() - message = f"{resp_status} - {ERRORS.get(resp_status, '')} - {resp_json['error']['message']} ({resp_json['error']['code']}) when accessing '{url}'" + message = (f"{resp_status} - {ERRORS.get(resp_status, '')} - {resp_json['error']['message']} " + f"({resp_json['error']['code']}) when accessing '{url}'") if resp_status == 403 and resp_json['error']['code'] == 26: raise ApiErrorThrottling(message, ) diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index a025e289..feeb50ab 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -114,6 +114,7 @@ ENERGY_ELEC_PEAK_IDX = 0 ENERGY_ELEC_OFF_IDX = 1 + class MeasureType(Enum): """Measure type.""" @@ -141,9 +142,10 @@ class MeasureInterval(Enum): WEEK = "1week" MONTH = "1month" -MEASURE_INTERVAL_TO_SECONDS = {MeasureInterval.HALF_HOUR:1800, - MeasureInterval.HOUR:3600, - MeasureInterval.THREE_HOURS:10800, - MeasureInterval.DAY:86400, - MeasureInterval.WEEK:604800, - MeasureInterval.MONTH:2592000} \ No newline at end of file + +MEASURE_INTERVAL_TO_SECONDS = {MeasureInterval.HALF_HOUR: 1800, + MeasureInterval.HOUR: 3600, + MeasureInterval.THREE_HOURS: 10800, + MeasureInterval.DAY: 86400, + MeasureInterval.WEEK: 604800, + MeasureInterval.MONTH: 2592000} diff --git a/src/pyatmo/exceptions.py b/src/pyatmo/exceptions.py index 1833e5e4..fa0a2fea 100644 --- a/src/pyatmo/exceptions.py +++ b/src/pyatmo/exceptions.py @@ -36,6 +36,7 @@ class ApiError(Exception): pass + class ApiErrorThrottling(ApiError): """Raised when an API error is encountered.""" diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 72c5a822..9db2adbd 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -33,7 +33,6 @@ def fix_id(raw_data: RawData) -> dict[str, Any]: def extract_raw_data(resp: Any, tag: str) -> dict[str, Any]: """Extract raw data from server response.""" - raw_data = {} if tag == "body": return {"public": resp["body"], "errors": []} diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 2175b694..e58f4fe5 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -40,8 +40,8 @@ class Home: name: str rooms: dict[str, Room] modules: dict[str, Module] - schedules: dict[str, ThermSchedule] #for compatibility should diseappear - all_schedules: dict[dict[str, str, Schedule]] + schedules: dict[str, ThermSchedule] # for compatibility should diseappear + all_schedules: dict[dict[str, str, Schedule]] | {} persons: dict[str, Person] events: dict[str, Event] energy_endpoints: list[str] @@ -76,11 +76,11 @@ def _handle_schedules(self, raw_data): schedules = {} for s in raw_data: - #strange but Energy plan are stored in schedules, we should handle this one differently - sched, type = schedule_factory(home=self, raw_data=s) - if type not in schedules: - schedules[type] = {} - schedules[type][s["id"]] = sched + # strange but Energy plan are stored in schedules, we should handle this one differently + sched, schedule_type = schedule_factory(home=self, raw_data=s) + if schedule_type not in schedules: + schedules[schedule_type] = {} + schedules[schedule_type][s["id"]] = sched self.schedules = schedules.get(SCHEDULE_TYPE_THERM, {}) self.all_schedules = schedules @@ -88,15 +88,15 @@ def _handle_schedules(self, raw_data): nrj_schedule = next(iter(schedules.get(SCHEDULE_TYPE_ELECTRICITY, {}).values()), None) self.energy_schedule_vals = [] - self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] + self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] if nrj_schedule is not None: + + # Tariff option (basic = always the same price, peak_and_off_peak = peak & off peak hours) type_tariff = nrj_schedule.tariff_option zones = nrj_schedule.zones if type_tariff == "peak_and_off_peak" and len(zones) >= 2: - - self.energy_endpoints = [None, None] self.energy_endpoints[ENERGY_ELEC_PEAK_IDX] = MeasureType.SUM_ENERGY_ELEC_PEAK.value @@ -104,18 +104,15 @@ def _handle_schedules(self, raw_data): if zones[0].price_type == "peak": peak_id = zones[0].entity_id - off_peak_id = zones[1].entity_id - else: peak_id = zones[1].entity_id - off_peak_id = zones[0].entity_id timetable = nrj_schedule.timetable - #timetable are daily for electricity type, and sorted from begining to end + # timetable are daily for electricity type, and sorted from begining to end for t in timetable: - time = t.m_offset*60 #m_offset is in minute from the begininng of the day + time = t.m_offset*60 # m_offset is in minute from the begininng of the day if len(self.energy_schedule_vals) == 0: time = 0 @@ -123,14 +120,11 @@ def _handle_schedules(self, raw_data): if t.zone_id == peak_id: pos_to_add = ENERGY_ELEC_PEAK_IDX - self.energy_schedule_vals.append((time,pos_to_add)) - + self.energy_schedule_vals.append((time, pos_to_add)) else: self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] - - def get_module(self, module: dict) -> Module: """Return module.""" @@ -214,12 +208,12 @@ async def update(self, raw_data: RawData) -> None: ], ) - def get_selected_schedule(self, type :str = None) -> Schedule | None: + def get_selected_schedule(self, schedule_type: str = None) -> Schedule | None: """Return selected schedule for given home.""" - if type is None: - type = SCHEDULE_TYPE_THERM + if schedule_type is None: + schedule_type = SCHEDULE_TYPE_THERM - schedules = self.all_schedules.get(type, {}) + schedules = self.all_schedules.get(schedule_type, {}) return next( (schedule for schedule in schedules.values() if schedule.selected), @@ -227,7 +221,7 @@ def get_selected_schedule(self, type :str = None) -> Schedule | None: ) def get_selected_temperature_schedule(self) -> ThermSchedule | None: - return self.get_selected_schedule(type=SCHEDULE_TYPE_THERM) + return self.get_selected_schedule(schedule_type=SCHEDULE_TYPE_THERM) def is_valid_schedule(self, schedule_id: str) -> bool: """Check if valid schedule.""" diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 7ca98378..a636edb0 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -56,9 +56,12 @@ class EntityBase: bridge: str | None history_features: set[str] history_features_values: dict[str, [int, int]] | {} + name: str | None -MAX_HISTORY_TIME_S = 24*2*3600 #2 days of dynamic historical data stored +# 2 days of dynamic historical data stored +MAX_HISTORY_TIME_S = 24*2*3600 + class NetatmoBase(EntityBase, ABC): """Base class for Netatmo entities.""" @@ -71,7 +74,6 @@ def __init__(self, raw_data: RawData) -> None: self.history_features_values = {} self.history_features = set() - def update_topology(self, raw_data: RawData) -> None: """Update topology.""" @@ -113,18 +115,15 @@ def _update_attributes(self, raw_data: RawData) -> None: i = None if i is not None: - hist_f.insert(i, (now,val, self.entity_id)) + hist_f.insert(i, (now, val, self.entity_id)) - #keep timing history to a maximum representative time + # keep timing history to a maximum representative time while len(hist_f) > 0 and now - hist_f[0][0] > MAX_HISTORY_TIME_S: hist_f.pop(0) - #LOG.debug(">>>>>> Features History : %s : %s", hist_feature, hist_f) - - - def get_history_data(self, type:str, from_ts: int, to_ts: int | None=None): + def get_history_data(self, feature: str, from_ts: int, to_ts: int | None = None): - hist_f = self.history_features_values.get(type, []) + hist_f = self.history_features_values.get(feature, []) if not hist_f: return [] @@ -139,10 +138,6 @@ def get_history_data(self, type:str, from_ts: int, to_ts: int | None=None): return hist_f[in_s:out_s] - - - - @dataclass class Location: """Class of Netatmo public weather location.""" diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index e388ad0a..ada0323e 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -451,7 +451,6 @@ def __init__(self, home: Home, module: ModuleT): self.local_url: str | None = None self.is_local: bool | None = None self.alim_status: int | None = None - self.device_type: DeviceType async def async_get_live_snapshot(self) -> bytes | None: """Fetch live camera image.""" @@ -469,7 +468,7 @@ async def async_get_live_snapshot(self) -> bytes | None: async def async_update_camera_urls(self) -> None: """Update and validate the camera urls.""" - if self.device_type == "NDB": + if isinstance(self, Module) and self.device_type == "NDB": self.is_local = None if self.vpn_url and self.is_local: @@ -583,6 +582,15 @@ async def async_monitoring_off(self) -> bool: return await self.async_set_monitoring_state("off") +def _get_proper_in_schedule_index(energy_schedule_vals, srt_beg): + idx = bisect.bisect_left(energy_schedule_vals, srt_beg, key=itemgetter(0)) + if idx >= len(energy_schedule_vals): + idx = len(energy_schedule_vals) - 1 + elif energy_schedule_vals[idx][0] > srt_beg: # if strict equal idx is the good one + idx = max(0, idx - 1) + return idx + + class EnergyHistoryMixin(EntityBase): """Mixin for history data.""" @@ -599,7 +607,7 @@ def __init__(self, home: Home, module: ModuleT): self.sum_energy_elec_off_peak: int | None = None self.last_computed_start: int | None = None self.last_computed_end: int | None = None - self.in_reset: bool| False = False + self.in_reset: bool | False = False def reset_measures(self): self.in_reset = True @@ -610,7 +618,6 @@ def reset_measures(self): self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None): v = self.sum_energy_elec @@ -627,43 +634,41 @@ def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None): from_ts = self.last_computed_end - if from_ts is not None and from_ts < to_ts and isinstance(self, PowerMixin) and isinstance(self, NetatmoBase): + if (from_ts is not None and from_ts < to_ts and + isinstance(self, PowerMixin) and isinstance(self, NetatmoBase)): power_data = self.get_history_data("power", from_ts=from_ts, to_ts=to_ts) if len(power_data) > 1: - #compute a rieman sum, as best as possible , trapezoidal, taking pessimistic asumption as we don't want to artifically go up the previous one (except in rare exceptions like reset, 0 , etc) + # compute a rieman sum, as best as possible , trapezoidal, taking pessimistic asumption + # as we don't want to artifically go up the previous one + # (except in rare exceptions like reset, 0 , etc) for i in range(len(power_data) - 1): dt_h = float(power_data[i+1][0] - power_data[i][0])/3600.0 - dP_W = abs(float(power_data[i+1][1] - power_data[i][1])) - - dNrj_Wh = dt_h*( min(power_data[i+1][1], power_data[i][1]) + 0.5*dP_W) + d_p_w = abs(float(power_data[i+1][1] - power_data[i][1])) - delta_energy += dNrj_Wh + d_nrj_wh = dt_h*(min(power_data[i+1][1], power_data[i][1]) + 0.5*d_p_w) + delta_energy += d_nrj_wh return v, delta_energy - - def _log_energy_error(self, start_time, end_time, msg=None, body=None): if body is None: body = "NO BODY" - LOG.debug("!!!!!!!!! ENERGY error %s %s %s %s", - msg, - self.name, - datetime.fromtimestamp(start_time), - datetime.fromtimestamp(end_time), start_time, end_time, body) + LOG.debug("ENERGY collection error %s %s %s %s", msg, self.name, + datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), start_time, end_time, body) def update_measures_num_calls(self): - try: - return len(self.home.energy_endpoints) - except: + + if not self.home.energy_endpoints: return 1 + else: + return len(self.home.energy_endpoints) async def async_update_measures( self, @@ -682,19 +687,19 @@ async def async_update_measures( start_time = end - timedelta(days=days) start_time = int(start_time.timestamp()) - prev_start_time = self.start_time prev_end_time = self.end_time self.start_time = start_time self.end_time = end_time - #the legrand/netatmo handling of start and endtime is very peculiar - #for 30mn/1h/3h intervals : in fact the starts is asked_start + intervals/2 ! yes so shift of 15mn, 30mn and 1h30 - #for 1day : start is ALWAYS 12am (half day) of the first day of the range - #for 1week : it will be half week ALWAYS, ie on a thursday at 12am (half day) + # the legrand/netatmo handling of start and endtime is very peculiar + # for 30mn/1h/3h intervals : in fact the starts is asked_start + intervals/2 ! + # => so shift of 15mn, 30mn and 1h30 + # for 1day : start is ALWAYS 12am (half day) of the first day of the range + # for 1week : it will be half week ALWAYS, ie on a thursday at 12am (half day) + # in fact in the case for all intervals the reported dates are "the middle" of the ranges - #in fact in the case for all intervals the reported dates are "the middle" of the ranges delta_range = MEASURE_INTERVAL_TO_SECONDS.get(interval, 0)//2 num_calls = 0 @@ -702,8 +707,6 @@ async def async_update_measures( data_points = self.home.energy_endpoints raw_datas = [] - #LOG.debug("INFO: doing async_update_measures for %s", self.name) - for data_point in data_points: params = { @@ -715,7 +718,6 @@ async def async_update_measures( "date_end": end_time, } - resp = await self.home.auth.async_post_api_request( endpoint=GETMEASURE_ENDPOINT, params=params, @@ -732,11 +734,9 @@ async def async_update_measures( f"when accessing '{data_point}'" ) - num_calls +=1 + num_calls += 1 raw_datas.append(rw_dt) - - hist_good_vals = [] energy_schedule_vals = [] @@ -748,101 +748,100 @@ async def async_update_measures( if peak_off_peak_mode: max_interval_sec = interval_sec - for cur_energy_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): + for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: - try: - max_interval_sec = max(max_interval_sec, int(values_lot["step_time"])) - except: + local_step_time = values_lot.get("step_time") + + if local_step_time is None: if len(values_lot.get("value", [])) > 1: self._log_energy_error(start_time, end_time, - msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", - body=raw_datas[cur_energy_peak_or_off_peak_mode]) - + msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode]) + else: + local_step_time = int(local_step_time) + max_interval_sec = max(max_interval_sec, local_step_time) - biggest_day_interval = (max_interval_sec)//(3600*24) + 1 + biggest_day_interval = max_interval_sec//(3600*24) + 1 energy_schedule_vals = copy.copy(self.home.energy_schedule_vals) if energy_schedule_vals[-1][0] < max_interval_sec + (3600*24): if energy_schedule_vals[0][1] == energy_schedule_vals[-1][1]: - #it means the last one continue in the first one the next day + # it means the last one continue in the first one the next day energy_schedule_vals_next = energy_schedule_vals[1:] else: energy_schedule_vals_next = copy.copy(self.home.energy_schedule_vals) for d in range(0, biggest_day_interval): - next_day_extension = [ (offset + ((d+1)*24*3600), mode) for offset,mode in energy_schedule_vals_next] - energy_schedule_vals.extend(next_day_extension) - - + next_day_extend = [(offset + ((d+1)*24*3600), mode) for offset, mode in energy_schedule_vals_next] + energy_schedule_vals.extend(next_day_extend) - for cur_energy_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): + for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: try: start_lot_time = int(values_lot["beg_time"]) - except: + except Exception: self._log_energy_error(start_time, end_time, - msg=f"beg_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", - body=raw_datas[cur_energy_peak_or_off_peak_mode]) + msg=f"beg_time missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode]) raise ApiError( - f"Energy badly formed resp beg_time missing: {raw_datas[cur_energy_peak_or_off_peak_mode]} - " + f"Energy badly formed resp beg_time missing: {raw_datas[cur_peak_or_off_peak_mode]} - " f"module: {self.name} - " - f"when accessing '{data_points[cur_energy_peak_or_off_peak_mode]}'" + f"when accessing '{data_points[cur_peak_or_off_peak_mode]}'" ) - try: - interval_sec = int(values_lot["step_time"]) - except: + interval_sec = values_lot.get("step_time") + if interval_sec is None: if len(values_lot.get("value", [])) > 1: - self._log_energy_error(start_time, end_time, msg=f"step_time missing {data_points[cur_energy_peak_or_off_peak_mode]}", body=raw_datas[cur_energy_peak_or_off_peak_mode]) + self._log_energy_error(start_time, end_time, + msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode]) interval_sec = 2*delta_range + else: + interval_sec = int(interval_sec) - - #align the start on the begining of the segment + # align the start on the begining of the segment cur_start_time = start_lot_time - interval_sec//2 - for val_arr in values_lot.get("value",[]): + for val_arr in values_lot.get("value", []): val = val_arr[0] - if peak_off_peak_mode: d_srt = datetime.fromtimestamp(cur_start_time) - #offset from start of the day + # offset from start of the day day_origin = int(datetime(d_srt.year, d_srt.month, d_srt.day).timestamp()) srt_beg = cur_start_time - day_origin srt_mid = srt_beg + interval_sec//2 - #now check if srt_beg is in a schedule span of the right type - idx_limit = self._get_proper_in_schedule_index(energy_schedule_vals, srt_mid) + # now check if srt_beg is in a schedule span of the right type + idx_limit = _get_proper_in_schedule_index(energy_schedule_vals, srt_mid) - if self.home.energy_schedule_vals[idx_limit][1] != cur_energy_peak_or_off_peak_mode: + if self.home.energy_schedule_vals[idx_limit][1] != cur_peak_or_off_peak_mode: - #we are NOT in a proper schedule time for this time span ... jump to the next one... meaning it is the next day! + # we are NOT in a proper schedule time for this time span ... + # jump to the next one... meaning it is the next day! if idx_limit == len(energy_schedule_vals) - 1: - #should never append with the performed day extension above + # should never append with the performed day extension above self._log_energy_error(start_time, end_time, - msg=f"bad idx missing {data_points[cur_energy_peak_or_off_peak_mode]}", - body=raw_datas[cur_energy_peak_or_off_peak_mode]) + msg=f"bad idx missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode]) return 0 else: - #by construction of the energy schedule the next one should be of opposite mode - if energy_schedule_vals[idx_limit + 1][1] != cur_energy_peak_or_off_peak_mode: + # by construction of the energy schedule the next one should be of opposite mode + if energy_schedule_vals[idx_limit + 1][1] != cur_peak_or_off_peak_mode: self._log_energy_error(start_time, end_time, - msg=f"bad schedule {data_points[cur_energy_peak_or_off_peak_mode]}", - body=raw_datas[cur_energy_peak_or_off_peak_mode]) + msg=f"bad schedule {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode]) return 0 - - start_time_to_get_closer = energy_schedule_vals[idx_limit+1][0] diff_t = start_time_to_get_closer - srt_mid cur_start_time = day_origin + srt_beg + (diff_t//interval_sec + 1)*interval_sec - hist_good_vals.append((cur_start_time, int(val), cur_energy_peak_or_off_peak_mode)) + hist_good_vals.append((cur_start_time, int(val), cur_peak_or_off_peak_mode)) cur_start_time = cur_start_time + interval_sec - hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) self.historical_data = [] @@ -852,12 +851,13 @@ async def async_update_measures( self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 self.last_computed_start = start_time - self.last_computed_end = start_time # no data at all : we know nothing for the end : best guess .. it is the start + self.last_computed_end = start_time # no data at all: we know nothing for the end: best guess, it is the start self.in_reset = False if len(hist_good_vals) == 0: - #nothing has been updated or changed it can nearly be seen as an error, but the api is answering correctly - #so we probably have to reset to 0 anyway as it means there were no exisitng historical data for this time range + # nothing has been updated or changed it can nearly be seen as an error, but the api is answering correctly + # so we probably have to reset to 0 anyway as it means there were no exisitng + # historical data for this time range LOG.debug( "=> NO VALUES energy update %s from: %s to %s, prev_sum=%s", @@ -867,13 +867,13 @@ async def async_update_measures( computed_start = 0 computed_end = 0 - for cur_start_time, val, cur_energy_peak_or_off_peak_mode in hist_good_vals: + for cur_start_time, val, cur_peak_or_off_peak_mode in hist_good_vals: self.sum_energy_elec += val if peak_off_peak_mode: mode = "off_peak" - if cur_energy_peak_or_off_peak_mode == ENERGY_ELEC_PEAK_IDX: + if cur_peak_or_off_peak_mode == ENERGY_ELEC_PEAK_IDX: self.sum_energy_elec_peak += val mode = "peak" else: @@ -881,7 +881,6 @@ async def async_update_measures( else: mode = "standard" - c_start = cur_start_time c_end = cur_start_time + 2*delta_range @@ -889,12 +888,13 @@ async def async_update_measures( computed_start = c_start computed_end = c_end - + start_time_string = f"{datetime.fromtimestamp(c_start + 1, tz=timezone.utc).isoformat().split('+')[0]}Z" + end_time_string = f"{datetime.fromtimestamp(c_end, tz=timezone.utc).isoformat().split('+')[0]}Z" self.historical_data.append( { "duration": (2*delta_range)//60, - "startTime": f"{datetime.fromtimestamp(c_start + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", - "endTime": f"{datetime.fromtimestamp(c_end, tz=timezone.utc).isoformat().split('+')[0]}Z", + "startTime": start_time_string, + "endTime": end_time_string, "Wh": val, "energyMode": mode, "startTimeUnix": c_start, @@ -903,16 +903,21 @@ async def async_update_measures( }, ) - if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: + msg = ("ENERGY GOING DOWN %s from: %s to %s " + "computed_start: %s, computed_end: %s, " + "sum=%f prev_sum=%f prev_start: %s, prev_end %s") LOG.debug( - ">>>>>>>>>> ENERGY GOING DOWN %s from: %s to %s computed_start: %s, computed_end: %s , sum=%f prev_sum=%f prev_start: %s, prev_end %s", + msg, self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, - prev_sum_energy_elec, datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) + prev_sum_energy_elec, + datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) else: + msg = ("Success in energy update %s from: %s to %s " + "computed_start: %s, computed_end: %s , sum=%s prev_sum=%s") LOG.debug( - "=> Success in energy update %s from: %s to %s computed_start: %s, computed_end: %s , sum=%s prev_sum=%s", + msg, self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") @@ -922,14 +927,6 @@ async def async_update_measures( return num_calls - def _get_proper_in_schedule_index(self, energy_schedule_vals, srt_beg): - idx = bisect.bisect_left(energy_schedule_vals, srt_beg, key=itemgetter(0)) - if idx >= len(energy_schedule_vals): - idx = len(energy_schedule_vals) - 1 - elif energy_schedule_vals[idx][0] > srt_beg: # if strict equal idx is the good one - idx = max(0, idx - 1) - return idx - class Module(NetatmoBase): """Class to represent a Netatmo module.""" diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index 8b6c20fe..b8f178cd 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -40,7 +40,6 @@ def __init__(self, home: Home, raw_data: RawData) -> None: self.zones = [Zone(home, r) for r in raw_data.get("zones", [])] - @dataclass class ScheduleWithRealZones(Schedule): """Class to represent a Netatmo schedule.""" @@ -66,7 +65,6 @@ def __init__(self, home: Home, raw_data: RawData) -> None: self.away_temp = raw_data.get("away_temp") - @dataclass class CoolingSchedule(ThermSchedule): """Class to represent a Netatmo Cooling schedule.""" @@ -78,6 +76,7 @@ def __init__(self, home: Home, raw_data: RawData) -> None: super().__init__(home, raw_data) self.cooling_away_temp = self.away_temp = raw_data.get("cooling_away_temp", self.away_temp) + @dataclass class ElectricitySchedule(Schedule): """Class to represent a Netatmo Energy Plan schedule.""" @@ -85,26 +84,26 @@ class ElectricitySchedule(Schedule): tariff: str tariff_option: str power_threshold: int | 6 - contract_power_unit: str #kVA or KW + contract_power_unit: str # kVA or KW zones: list[ZoneElectricity] def __init__(self, home: Home, raw_data: RawData) -> None: super().__init__(home, raw_data) self.tariff = raw_data.get("tariff", "custom") - self.tariff_option = raw_data.get("tariff_option", None) + # Tariff option (basic = always the same price, peak_and_off_peak = peak & off peak hours) + self.tariff_option = raw_data.get("tariff_option", "basic") self.power_threshold = raw_data.get("power_threshold", 6) self.contract_power_unit = raw_data.get("power_threshold", "kVA") self.zones = [ZoneElectricity(home, r) for r in raw_data.get("zones", [])] - - @dataclass class EventSchedule(Schedule): """Class to represent a Netatmo Energy Plan schedule.""" timetable_sunrise: list[TimetableEventEntry] timetable_sunset: list[TimetableEventEntry] + def __init__(self, home: Home, raw_data: RawData) -> None: super().__init__(home, raw_data) self.timetable_sunrise = [ @@ -114,6 +113,7 @@ def __init__(self, home: Home, raw_data: RawData) -> None: TimetableEventEntry(home, r) for r in raw_data.get("timetable_sunset", []) ] + @dataclass class TimetableEntry: """Class to represent a Netatmo schedule's timetable entry.""" @@ -136,6 +136,7 @@ class TimetableEventEntry: zone_id: int | None day: int | 1 twilight_offset: int | 0 + def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule's timetable entry instance.""" self.home = home @@ -144,13 +145,12 @@ def __init__(self, home: Home, raw_data: RawData) -> None: self.twilight_offset = raw_data.get("twilight_offset", 0) - class ModuleSchedule(NetatmoBase): - on: bool - target_position: int - fan_speed: int - brightness: int + on: bool | None + target_position: int | None + fan_speed: int | None + brightness: int | None def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule's zone instance.""" @@ -170,15 +170,14 @@ class Zone(NetatmoBase): rooms: list[Room] modules: list[ModuleSchedule] - def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule's zone instance.""" super().__init__(raw_data) self.home = home self.type = raw_data.get("type", 0) - def room_factory(home: Home, room_raw_data: RawData): - room = Room(home, room_raw_data, {}) + def room_factory(room_home: Home, room_raw_data: RawData): + room = Room(room_home, room_raw_data, {}) room.update(room_raw_data) return room @@ -202,7 +201,9 @@ def __init__(self, home: Home, raw_data: RawData) -> None: def schedule_factory(home: Home, raw_data: RawData) -> (Schedule, str): - type = raw_data.get("type", "custom") - cls = {SCHEDULE_TYPE_THERM: ThermSchedule, SCHEDULE_TYPE_EVENT: EventSchedule, SCHEDULE_TYPE_ELECTRICITY: ElectricitySchedule, SCHEDULE_TYPE_COOLING: CoolingSchedule}.get(type, Schedule) - return cls(home, raw_data), type - + schedule_type = raw_data.get("type", "custom") + cls = {SCHEDULE_TYPE_THERM: ThermSchedule, + SCHEDULE_TYPE_EVENT: EventSchedule, + SCHEDULE_TYPE_ELECTRICITY: ElectricitySchedule, + SCHEDULE_TYPE_COOLING: CoolingSchedule}.get(schedule_type, Schedule) + return cls(home, raw_data), schedule_type diff --git a/tests/test_energy.py b/tests/test_energy.py index 387a259e..d9496d37 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -54,9 +54,6 @@ async def test_historical_data_retrieval_multi(async_account_multi): module = home.modules[module_id] assert module.device_type == DeviceType.NLC - strt = 1709421000 - end_time = 1709679599 - strt = int(dt.datetime.fromisoformat("2024-03-03 00:10:00").timestamp()) end_time = int(dt.datetime.fromisoformat("2024-03-05 23:59:59").timestamp()) @@ -66,7 +63,6 @@ async def test_historical_data_retrieval_multi(async_account_multi): start_time=strt, end_time=end_time ) - assert isinstance(module, EnergyHistoryMixin) assert module.historical_data[0] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-02T23:40:00Z', 'endTimeUnix': 1709422800, 'energyMode': 'peak', 'startTime': '2024-03-02T23:10:01Z', 'startTimeUnix': 1709421000} @@ -77,8 +73,8 @@ async def test_historical_data_retrieval_multi(async_account_multi): assert module.sum_energy_elec_off_peak == 11219 assert module.sum_energy_elec_peak == 31282 - assert module.sum_energy_elec == async_account_multi.get_current_energy_sum() - assert async_account_multi.get_current_energy_sum(excluded_modules={module_id}) == 0 + assert module.sum_energy_elec == async_account_multi.get_current_energy_sum(ok_if_none = True)[0] + assert async_account_multi.get_current_energy_sum(excluded_modules={module_id}, ok_if_none = True)[0] == 0 @@ -120,6 +116,6 @@ async def test_historical_data_retrieval_multi_2(async_account_multi): assert module.sum_energy_elec_peak == 890 - sum = async_account_multi.get_current_energy_sum() + sum, _ = async_account_multi.get_current_energy_sum(ok_if_none = True) assert module.sum_energy_elec == sum From 25d7bfa88f86dff30d4953461bfbafe1f72b9a19 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 4 Apr 2024 12:34:38 +0200 Subject: [PATCH 47/97] adjusting estimation calculus --- src/pyatmo/modules/module.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index ada0323e..e0c37922 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -605,15 +605,13 @@ def __init__(self, home: Home, module: ModuleT): self.sum_energy_elec: int | None = None self.sum_energy_elec_peak: int | None = None self.sum_energy_elec_off_peak: int | None = None - self.last_computed_start: int | None = None - self.last_computed_end: int | None = None + self._last_energy_from_API_end_for_power_adjustment_calculus: int | None = None self.in_reset: bool | False = False def reset_measures(self): self.in_reset = True self.historical_data = [] - self.last_computed_start = None - self.last_computed_end = None + self._last_energy_from_API_end_for_power_adjustment_calculus = None self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 @@ -632,7 +630,7 @@ def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None): if to_ts is None: to_ts = time() - from_ts = self.last_computed_end + from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus if (from_ts is not None and from_ts < to_ts and isinstance(self, PowerMixin) and isinstance(self, NetatmoBase)): @@ -850,8 +848,7 @@ async def async_update_measures( self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - self.last_computed_start = start_time - self.last_computed_end = start_time # no data at all: we know nothing for the end: best guess, it is the start + self._last_energy_from_API_end_for_power_adjustment_calculus = start_time # no data at all: we know nothing for the end: best guess, it is the start self.in_reset = False if len(hist_good_vals) == 0: @@ -867,6 +864,7 @@ async def async_update_measures( computed_start = 0 computed_end = 0 + computed_end_for_calculus = 0 for cur_start_time, val, cur_peak_or_off_peak_mode in hist_good_vals: self.sum_energy_elec += val @@ -887,6 +885,7 @@ async def async_update_measures( if computed_start == 0: computed_start = c_start computed_end = c_end + computed_end_for_calculus = c_end - delta_range #it seems the energy value effectively stops at those mid values start_time_string = f"{datetime.fromtimestamp(c_start + 1, tz=timezone.utc).isoformat().split('+')[0]}Z" end_time_string = f"{datetime.fromtimestamp(c_end, tz=timezone.utc).isoformat().split('+')[0]}Z" @@ -922,8 +921,7 @@ async def async_update_measures( datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") - self.last_computed_start = computed_start - self.last_computed_end = computed_end + self._last_energy_from_API_end_for_power_adjustment_calculus = computed_end_for_calculus return num_calls From 0f8731e1a0b1441058a7ee4c3a096da76f302063 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 4 Apr 2024 13:14:36 +0200 Subject: [PATCH 48/97] adjusting estimation calculus --- src/pyatmo/account.py | 5 +++-- src/pyatmo/modules/module.py | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 89bfa765..23326636 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -175,7 +175,8 @@ def get_current_energy_sum(self, power_adapted: bool = True, to_ts: int | float | None = None, excluded_modules: set[str] | None = None, - ok_if_none: bool = False): + ok_if_none: bool = False, + conservative: bool = False): energy_sum = 0 is_in_reset = False @@ -194,7 +195,7 @@ def get_current_energy_sum(self, is_in_reset = True break if power_adapted: - v, delta_energy = module.get_sum_energy_elec_power_adapted(to_ts=to_ts) + v, delta_energy = module.get_sum_energy_elec_power_adapted(to_ts=to_ts, conservative=conservative) else: delta_energy = 0 v = module.sum_energy_elec diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index e0c37922..6813fac3 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -616,7 +616,7 @@ def reset_measures(self): self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None): + def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None, conservative: bool = False): v = self.sum_energy_elec @@ -647,9 +647,12 @@ def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None): dt_h = float(power_data[i+1][0] - power_data[i][0])/3600.0 - d_p_w = abs(float(power_data[i+1][1] - power_data[i][1])) + if conservative: + d_p_w = 0 + else: + d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) - d_nrj_wh = dt_h*(min(power_data[i+1][1], power_data[i][1]) + 0.5*d_p_w) + d_nrj_wh = dt_h*(min(power_data[i + 1][1], power_data[i][1]) + 0.5*d_p_w) delta_energy += d_nrj_wh From adca8693aba1ee68ab8ec2aae9aa680629a9710a Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 5 Apr 2024 00:50:28 +0200 Subject: [PATCH 49/97] adjusting estimation calculus --- src/pyatmo/modules/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 6813fac3..96e7a6ca 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -888,7 +888,7 @@ async def async_update_measures( if computed_start == 0: computed_start = c_start computed_end = c_end - computed_end_for_calculus = c_end - delta_range #it seems the energy value effectively stops at those mid values + computed_end_for_calculus = c_end # - delta_range #not sure, revert ... it seems the energy value effectively stops at those mid values start_time_string = f"{datetime.fromtimestamp(c_start + 1, tz=timezone.utc).isoformat().split('+')[0]}Z" end_time_string = f"{datetime.fromtimestamp(c_end, tz=timezone.utc).isoformat().split('+')[0]}Z" From ce496cf5c13fcdd4fcdc0535dfbf94efa9e844a1 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 9 Apr 2024 01:10:53 +0200 Subject: [PATCH 50/97] slight refactor for readability --- src/pyatmo/modules/module.py | 370 ++++++++++++++++++++--------------- 1 file changed, 210 insertions(+), 160 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 96e7a6ca..2f3038ed 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -616,6 +616,32 @@ def reset_measures(self): self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 + + + def compute_rieman_sum(self, power_data, conservative: bool = False): + + delta_energy = 0 + if len(power_data) > 1: + + # compute a rieman sum, as best as possible , trapezoidal, taking pessimistic asumption + # as we don't want to artifically go up the previous one + # (except in rare exceptions like reset, 0 , etc) + + for i in range(len(power_data) - 1): + + dt_h = float(power_data[i + 1][0] - power_data[i][0]) / 3600.0 + + if conservative: + d_p_w = 0 + else: + d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) + + d_nrj_wh = dt_h * (min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w) + + delta_energy += d_nrj_wh + + return delta_energy + def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None, conservative: bool = False): v = self.sum_energy_elec @@ -632,29 +658,11 @@ def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None, co from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus - if (from_ts is not None and from_ts < to_ts and - isinstance(self, PowerMixin) and isinstance(self, NetatmoBase)): + if (from_ts is not None and from_ts < to_ts and isinstance(self, PowerMixin) and isinstance(self, NetatmoBase)) : power_data = self.get_history_data("power", from_ts=from_ts, to_ts=to_ts) - if len(power_data) > 1: - - # compute a rieman sum, as best as possible , trapezoidal, taking pessimistic asumption - # as we don't want to artifically go up the previous one - # (except in rare exceptions like reset, 0 , etc) - - for i in range(len(power_data) - 1): - - dt_h = float(power_data[i+1][0] - power_data[i][0])/3600.0 - - if conservative: - d_p_w = 0 - else: - d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) - - d_nrj_wh = dt_h*(min(power_data[i + 1][1], power_data[i][1]) + 0.5*d_p_w) - - delta_energy += d_nrj_wh + delta_energy = self.compute_rieman_sum(power_data, conservative) return v, delta_energy @@ -703,80 +711,126 @@ async def async_update_measures( delta_range = MEASURE_INTERVAL_TO_SECONDS.get(interval, 0)//2 - num_calls = 0 + data_points, num_calls, raw_datas, peak_off_peak_mode = await self._energy_API_calls(start_time, end_time, interval) - data_points = self.home.energy_endpoints - raw_datas = [] + energy_schedule_vals = [] - for data_point in data_points: + if peak_off_peak_mode: + energy_schedule_vals = await self._compute_proper_energy_schedule_offsets(start_time, + end_time, + 2 * delta_range, + raw_datas, + data_points) + + hist_good_vals = await self._get_aligned_energy_values_and_mode(start_time, + end_time, + delta_range, + energy_schedule_vals, + peak_off_peak_mode, + raw_datas, + data_points) - params = { - "device_id": self.bridge, - "module_id": self.entity_id, - "scale": interval.value, - "type": data_point, - "date_begin": start_time, - "date_end": end_time, - } + self.historical_data = [] + prev_sum_energy_elec = self.sum_energy_elec + self.sum_energy_elec = 0 + self.sum_energy_elec_peak = 0 + self.sum_energy_elec_off_peak = 0 + self._last_energy_from_API_end_for_power_adjustment_calculus = start_time # no data at all: we know nothing for the end: best guess, it is the start + self.in_reset = False - resp = await self.home.auth.async_post_api_request( - endpoint=GETMEASURE_ENDPOINT, - params=params, - ) + if len(hist_good_vals) == 0: + # nothing has been updated or changed it can nearly be seen as an error, but the api is answering correctly + # so we probably have to reset to 0 anyway as it means there were no exisitng + # historical data for this time range - rw_dt_f = await resp.json() - rw_dt = rw_dt_f.get("body") + LOG.debug( + "=> NO VALUES energy update %s from: %s to %s, prev_sum=%s", + self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), + prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") + else: - if rw_dt is None: - self._log_energy_error(start_time, end_time, msg=f"direct from {data_point}", body=rw_dt_f) - raise ApiError( - f"Energy badly formed resp: {rw_dt_f} - " - f"module: {self.name} - " - f"when accessing '{data_point}'" - ) + await self._prepare_exported_historical_data(start_time, + end_time, + delta_range, + hist_good_vals, + prev_end_time, + prev_start_time, + prev_sum_energy_elec, + peak_off_peak_mode) - num_calls += 1 - raw_datas.append(rw_dt) + return num_calls - hist_good_vals = [] - energy_schedule_vals = [] + async def _prepare_exported_historical_data(self, + start_time, + end_time, + delta_range, + hist_good_vals, + prev_end_time, + prev_start_time, + prev_sum_energy_elec, + peak_off_peak_mode): + computed_start = 0 + computed_end = 0 + computed_end_for_calculus = 0 + for cur_start_time, val, cur_peak_or_off_peak_mode in hist_good_vals: + + self.sum_energy_elec += val + + if peak_off_peak_mode: + mode = "off_peak" + if cur_peak_or_off_peak_mode == ENERGY_ELEC_PEAK_IDX: + self.sum_energy_elec_peak += val + mode = "peak" + else: + self.sum_energy_elec_off_peak += val + else: + mode = "standard" - peak_off_peak_mode = False - if len(raw_datas) > 1 and len(self.home.energy_schedule_vals) > 0: - peak_off_peak_mode = True + c_start = cur_start_time + c_end = cur_start_time + 2 * delta_range - interval_sec = 2 * delta_range + if computed_start == 0: + computed_start = c_start + computed_end = c_end + computed_end_for_calculus = c_end # - delta_range #not sure, revert ... it seems the energy value effectively stops at those mid values - if peak_off_peak_mode: - max_interval_sec = interval_sec - for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): - for values_lot in values_lots: - local_step_time = values_lot.get("step_time") - - if local_step_time is None: - if len(values_lot.get("value", [])) > 1: - self._log_energy_error(start_time, end_time, - msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode]) - else: - local_step_time = int(local_step_time) - max_interval_sec = max(max_interval_sec, local_step_time) - - biggest_day_interval = max_interval_sec//(3600*24) + 1 - - energy_schedule_vals = copy.copy(self.home.energy_schedule_vals) - - if energy_schedule_vals[-1][0] < max_interval_sec + (3600*24): - if energy_schedule_vals[0][1] == energy_schedule_vals[-1][1]: - # it means the last one continue in the first one the next day - energy_schedule_vals_next = energy_schedule_vals[1:] - else: - energy_schedule_vals_next = copy.copy(self.home.energy_schedule_vals) + start_time_string = f"{datetime.fromtimestamp(c_start + 1, tz=timezone.utc).isoformat().split('+')[0]}Z" + end_time_string = f"{datetime.fromtimestamp(c_end, tz=timezone.utc).isoformat().split('+')[0]}Z" + self.historical_data.append( + { + "duration": (2 * delta_range) // 60, + "startTime": start_time_string, + "endTime": end_time_string, + "Wh": val, + "energyMode": mode, + "startTimeUnix": c_start, + "endTimeUnix": c_end - for d in range(0, biggest_day_interval): - next_day_extend = [(offset + ((d+1)*24*3600), mode) for offset, mode in energy_schedule_vals_next] - energy_schedule_vals.extend(next_day_extend) + }, + ) + if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: + msg = ("ENERGY GOING DOWN %s from: %s to %s " + "computed_start: %s, computed_end: %s, " + "sum=%f prev_sum=%f prev_start: %s, prev_end %s") + LOG.debug( + msg, + self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), + datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, + prev_sum_energy_elec, + datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) + else: + msg = ("Success in energy update %s from: %s to %s " + "computed_start: %s, computed_end: %s , sum=%s prev_sum=%s") + LOG.debug( + msg, + self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), + datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, + prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") + self._last_energy_from_API_end_for_power_adjustment_calculus = computed_end_for_calculus + async def _get_aligned_energy_values_and_mode(self, start_time, end_time, delta_range, energy_schedule_vals, + peak_off_peak_mode, raw_datas, data_points): + hist_good_vals = [] for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: try: @@ -797,12 +851,12 @@ async def async_update_measures( self._log_energy_error(start_time, end_time, msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", body=raw_datas[cur_peak_or_off_peak_mode]) - interval_sec = 2*delta_range + interval_sec = 2 * delta_range else: interval_sec = int(interval_sec) # align the start on the begining of the segment - cur_start_time = start_lot_time - interval_sec//2 + cur_start_time = start_lot_time - interval_sec // 2 for val_arr in values_lot.get("value", []): val = val_arr[0] @@ -812,7 +866,7 @@ async def async_update_measures( # offset from start of the day day_origin = int(datetime(d_srt.year, d_srt.month, d_srt.day).timestamp()) srt_beg = cur_start_time - day_origin - srt_mid = srt_beg + interval_sec//2 + srt_mid = srt_beg + interval_sec // 2 # now check if srt_beg is in a schedule span of the right type idx_limit = _get_proper_in_schedule_index(energy_schedule_vals, srt_mid) @@ -827,106 +881,102 @@ async def async_update_measures( msg=f"bad idx missing {data_points[cur_peak_or_off_peak_mode]}", body=raw_datas[cur_peak_or_off_peak_mode]) - return 0 + raise ApiError( + f"Energy badly formed bad schedule idx in vals: {raw_datas[cur_peak_or_off_peak_mode]} - " + f"module: {self.name} - " + f"when accessing '{data_points[cur_peak_or_off_peak_mode]}'" + ) else: # by construction of the energy schedule the next one should be of opposite mode if energy_schedule_vals[idx_limit + 1][1] != cur_peak_or_off_peak_mode: self._log_energy_error(start_time, end_time, msg=f"bad schedule {data_points[cur_peak_or_off_peak_mode]}", body=raw_datas[cur_peak_or_off_peak_mode]) - return 0 + raise ApiError( + f"Energy badly formed bad schedule: {raw_datas[cur_peak_or_off_peak_mode]} - " + f"module: {self.name} - " + f"when accessing '{data_points[cur_peak_or_off_peak_mode]}'" + ) - start_time_to_get_closer = energy_schedule_vals[idx_limit+1][0] + start_time_to_get_closer = energy_schedule_vals[idx_limit + 1][0] diff_t = start_time_to_get_closer - srt_mid - cur_start_time = day_origin + srt_beg + (diff_t//interval_sec + 1)*interval_sec + cur_start_time = day_origin + srt_beg + (diff_t // interval_sec + 1) * interval_sec hist_good_vals.append((cur_start_time, int(val), cur_peak_or_off_peak_mode)) cur_start_time = cur_start_time + interval_sec hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) + return hist_good_vals - self.historical_data = [] + async def _compute_proper_energy_schedule_offsets(self, start_time, end_time, interval_sec, raw_datas, data_points): + max_interval_sec = interval_sec + for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): + for values_lot in values_lots: + local_step_time = values_lot.get("step_time") - prev_sum_energy_elec = self.sum_energy_elec - self.sum_energy_elec = 0 - self.sum_energy_elec_peak = 0 - self.sum_energy_elec_off_peak = 0 - self._last_energy_from_API_end_for_power_adjustment_calculus = start_time # no data at all: we know nothing for the end: best guess, it is the start - self.in_reset = False + if local_step_time is None: + if len(values_lot.get("value", [])) > 1: + self._log_energy_error(start_time, end_time, + msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode]) + else: + local_step_time = int(local_step_time) + max_interval_sec = max(max_interval_sec, local_step_time) + biggest_day_interval = max_interval_sec // (3600 * 24) + 1 + energy_schedule_vals = copy.copy(self.home.energy_schedule_vals) + if energy_schedule_vals[-1][0] < max_interval_sec + (3600 * 24): + if energy_schedule_vals[0][1] == energy_schedule_vals[-1][1]: + # it means the last one continue in the first one the next day + energy_schedule_vals_next = energy_schedule_vals[1:] + else: + energy_schedule_vals_next = copy.copy(self.home.energy_schedule_vals) - if len(hist_good_vals) == 0: - # nothing has been updated or changed it can nearly be seen as an error, but the api is answering correctly - # so we probably have to reset to 0 anyway as it means there were no exisitng - # historical data for this time range + for d in range(0, biggest_day_interval): + next_day_extend = [(offset + ((d + 1) * 24 * 3600), mode) for offset, mode in energy_schedule_vals_next] + energy_schedule_vals.extend(next_day_extend) + return energy_schedule_vals - LOG.debug( - "=> NO VALUES energy update %s from: %s to %s, prev_sum=%s", - self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), - prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") - else: + async def _energy_API_calls(self, start_time, end_time, interval): + num_calls = 0 + data_points = self.home.energy_endpoints + raw_datas = [] + for data_point in data_points: - computed_start = 0 - computed_end = 0 - computed_end_for_calculus = 0 - for cur_start_time, val, cur_peak_or_off_peak_mode in hist_good_vals: + params = { + "device_id": self.bridge, + "module_id": self.entity_id, + "scale": interval.value, + "type": data_point, + "date_begin": start_time, + "date_end": end_time, + } - self.sum_energy_elec += val + resp = await self.home.auth.async_post_api_request( + endpoint=GETMEASURE_ENDPOINT, + params=params, + ) - if peak_off_peak_mode: - mode = "off_peak" - if cur_peak_or_off_peak_mode == ENERGY_ELEC_PEAK_IDX: - self.sum_energy_elec_peak += val - mode = "peak" - else: - self.sum_energy_elec_off_peak += val - else: - mode = "standard" - - c_start = cur_start_time - c_end = cur_start_time + 2*delta_range - - if computed_start == 0: - computed_start = c_start - computed_end = c_end - computed_end_for_calculus = c_end # - delta_range #not sure, revert ... it seems the energy value effectively stops at those mid values - - start_time_string = f"{datetime.fromtimestamp(c_start + 1, tz=timezone.utc).isoformat().split('+')[0]}Z" - end_time_string = f"{datetime.fromtimestamp(c_end, tz=timezone.utc).isoformat().split('+')[0]}Z" - self.historical_data.append( - { - "duration": (2*delta_range)//60, - "startTime": start_time_string, - "endTime": end_time_string, - "Wh": val, - "energyMode": mode, - "startTimeUnix": c_start, - "endTimeUnix": c_end - - }, + rw_dt_f = await resp.json() + rw_dt = rw_dt_f.get("body") + + if rw_dt is None: + self._log_energy_error(start_time, end_time, msg=f"direct from {data_point}", body=rw_dt_f) + raise ApiError( + f"Energy badly formed resp: {rw_dt_f} - " + f"module: {self.name} - " + f"when accessing '{data_point}'" ) - if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: - msg = ("ENERGY GOING DOWN %s from: %s to %s " - "computed_start: %s, computed_end: %s, " - "sum=%f prev_sum=%f prev_start: %s, prev_end %s") - LOG.debug( - msg, - self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), - datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, - prev_sum_energy_elec, - datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) - else: - msg = ("Success in energy update %s from: %s to %s " - "computed_start: %s, computed_end: %s , sum=%s prev_sum=%s") - LOG.debug( - msg, - self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), - datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, - prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") + num_calls += 1 + raw_datas.append(rw_dt) - self._last_energy_from_API_end_for_power_adjustment_calculus = computed_end_for_calculus - return num_calls + peak_off_peak_mode = False + if len(raw_datas) > 1 and len(self.home.energy_schedule_vals) > 0: + peak_off_peak_mode = True + + + return data_points, num_calls, raw_datas, peak_off_peak_mode class Module(NetatmoBase): From 4a9c9fb0f34e0fa4f25b065eb73eb05147053b33 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 9 Apr 2024 17:47:12 +0200 Subject: [PATCH 51/97] Support error for unreachable home and bridges ....it is an error, not a normal call --- .../home_multi_status_error_disconnected.json | 15 +++++++++ src/pyatmo/__init__.py | 3 +- src/pyatmo/account.py | 9 ++++- src/pyatmo/exceptions.py | 6 ++++ src/pyatmo/home.py | 24 ++++++++++++-- tests/test_energy.py | 33 ++++++++++++++++++- 6 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 fixtures/home_multi_status_error_disconnected.json diff --git a/fixtures/home_multi_status_error_disconnected.json b/fixtures/home_multi_status_error_disconnected.json new file mode 100644 index 00000000..f94595bf --- /dev/null +++ b/fixtures/home_multi_status_error_disconnected.json @@ -0,0 +1,15 @@ +{ + "status":"ok", + "body":{ + "errors":[ + { + "code":6, + "id":"aa:aa:aa:aa:aa:aa" + } + ], + "home":{ + "id":"aaaaaaaaaaabbbbbbbbbbccc" + } + }, + "time_server":1559292039 +} \ No newline at end of file diff --git a/src/pyatmo/__init__.py b/src/pyatmo/__init__.py index 0f84744a..37585c0b 100644 --- a/src/pyatmo/__init__.py +++ b/src/pyatmo/__init__.py @@ -3,7 +3,7 @@ from pyatmo import const, modules from pyatmo.account import AsyncAccount from pyatmo.auth import AbstractAsyncAuth -from pyatmo.exceptions import ApiError, ApiErrorThrottling, InvalidHome, InvalidRoom, NoDevice, NoSchedule +from pyatmo.exceptions import ApiError, ApiErrorThrottling, ApiHomeReachabilityError, InvalidHome, InvalidRoom, NoDevice, NoSchedule from pyatmo.home import Home from pyatmo.modules import Module from pyatmo.modules.device_types import DeviceType @@ -13,6 +13,7 @@ "AbstractAsyncAuth", "ApiError", "ApiErrorThrottling", + "ApiHomeReachabilityError", "AsyncAccount", "InvalidHome", "InvalidRoom", diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 23326636..22339a7b 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -19,6 +19,7 @@ SETSTATE_ENDPOINT, RawData, MeasureInterval, ) +from pyatmo.exceptions import ApiHomeReachabilityError from pyatmo.helpers import extract_raw_data from pyatmo.home import Home from pyatmo.modules.module import Module @@ -115,15 +116,21 @@ async def async_update_status(self, home_id: str | None = None) -> int: else: homes = [home_id] num_calls = 0 + all_homes_ok = True for h_id in homes: resp = await self.auth.async_post_api_request( endpoint=GETHOMESTATUS_ENDPOINT, params={"home_id": h_id}, ) raw_data = extract_raw_data(await resp.json(), HOME) - await self.all_account_homes[h_id].update(raw_data) + is_correct_update = await self.all_account_homes[h_id].update(raw_data) + if not is_correct_update: + all_homes_ok = False num_calls += 1 + if all_homes_ok is False: + raise ApiHomeReachabilityError("No Home update could be performed, all modules unreachable and not updated", ) + return num_calls async def async_update_events(self, home_id: str) -> int: diff --git a/src/pyatmo/exceptions.py b/src/pyatmo/exceptions.py index fa0a2fea..31cc8c69 100644 --- a/src/pyatmo/exceptions.py +++ b/src/pyatmo/exceptions.py @@ -43,6 +43,12 @@ class ApiErrorThrottling(ApiError): pass +class ApiHomeReachabilityError(ApiError): + """Raised when an API error is encountered.""" + + pass + + class InvalidState(Exception): """Raised when an invalid state is encountered.""" diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index e58f4fe5..729e8560 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -176,27 +176,37 @@ def update_topology(self, raw_data: RawData) -> None: self._handle_schedules(raw_data.get(SCHEDULES, [])) - async def update(self, raw_data: RawData) -> None: + async def update(self, raw_data: RawData) -> bool: """Update home with the latest data.""" - + num_errors = 0 for module in raw_data.get("errors", []): + num_errors += 1 await self.modules[module["id"]].update({}) data = raw_data["home"] + num_updated_modules = 0 for module in data.get("modules", []): + num_updated_modules += 1 if module["id"] not in self.modules: self.update_topology({"modules": [module]}) await self.modules[module["id"]].update(module) + num_updated_rooms = 0 for room in data.get("rooms", []): + num_updated_rooms += 1 self.rooms[room["id"]].update(room) self.events = { s["id"]: Event(home_id=self.entity_id, raw_data=s) for s in data.get(EVENTS, []) } + num_events = len(self.events) + + has_one_module_reachable = False for module in self.modules.values(): + if module.reachable: + has_one_module_reachable = True if hasattr(module, "events"): setattr( module, @@ -208,6 +218,16 @@ async def update(self, raw_data: RawData) -> None: ], ) + if num_errors > 0 and has_one_module_reachable is False and num_updated_modules == 0 and num_updated_rooms == 0 and num_events == 0: + return False + + return True + + + + + + def get_selected_schedule(self, schedule_type: str = None) -> Schedule | None: """Return selected schedule for given home.""" if schedule_type is None: diff --git a/tests/test_energy.py b/tests/test_energy.py index d9496d37..d5ac1451 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -1,6 +1,6 @@ """Define tests for energy module.""" -from pyatmo import DeviceType +from pyatmo import DeviceType, ApiHomeReachabilityError import pytest import time_machine @@ -10,6 +10,14 @@ from pyatmo.modules.module import EnergyHistoryMixin +import json +from unittest.mock import AsyncMock, patch + +from pyatmo import DeviceType, NoSchedule +import pytest + +from tests.common import MockResponse, fake_post_request + # pylint: disable=F6401 @@ -119,3 +127,26 @@ async def test_historical_data_retrieval_multi_2(async_account_multi): sum, _ = async_account_multi.get_current_energy_sum(ok_if_none = True) assert module.sum_energy_elec == sum + +async def test_disconnected_main_bridge(async_account_multi): + """Test retrieval of historical measurements.""" + home_id = "aaaaaaaaaaabbbbbbbbbbccc" + + with open( + "fixtures/home_multi_status_error_disconnected.json", + encoding="utf-8", + ) as json_file: + home_status_fixture = json.load(json_file) + mock_home_status_resp = MockResponse(home_status_fixture, 200) + + with patch( + "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", + AsyncMock(return_value=mock_home_status_resp), + ) as mock_request: + try: + await async_account_multi.async_update_status(home_id) + except ApiHomeReachabilityError: + pass # expected error + else: + assert False + From 3e5c70eb33cbf1b5c8fd42baf16a2b9b5a85b441 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 9 Apr 2024 23:16:26 +0200 Subject: [PATCH 52/97] Support error for unreachable home and bridges ....it is an error, not a normal call --- src/pyatmo/home.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 729e8560..21b0ab4b 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -185,23 +185,23 @@ async def update(self, raw_data: RawData) -> bool: data = raw_data["home"] - num_updated_modules = 0 + has_an_update = False for module in data.get("modules", []): - num_updated_modules += 1 + has_an_update = True if module["id"] not in self.modules: self.update_topology({"modules": [module]}) await self.modules[module["id"]].update(module) - num_updated_rooms = 0 for room in data.get("rooms", []): - num_updated_rooms += 1 + has_an_update = True self.rooms[room["id"]].update(room) self.events = { s["id"]: Event(home_id=self.entity_id, raw_data=s) for s in data.get(EVENTS, []) } - num_events = len(self.events) + if len(self.events) > 0: + has_an_update = True has_one_module_reachable = False for module in self.modules.values(): @@ -218,7 +218,7 @@ async def update(self, raw_data: RawData) -> bool: ], ) - if num_errors > 0 and has_one_module_reachable is False and num_updated_modules == 0 and num_updated_rooms == 0 and num_events == 0: + if num_errors > 0 and has_one_module_reachable is False and has_an_update is False: return False return True From ce7ca84f3d94a14630c5853000a49f2d932d42a1 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 18 Apr 2024 16:10:00 +0200 Subject: [PATCH 53/97] remove global energy sum support, no needed anymore, was adding complexity --- src/pyatmo/account.py | 37 ------------------------------------- tests/test_energy.py | 7 ------- 2 files changed, 44 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 22339a7b..79034491 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -178,43 +178,6 @@ async def async_update_measures( ) return num_calls - def get_current_energy_sum(self, - power_adapted: bool = True, - to_ts: int | float | None = None, - excluded_modules: set[str] | None = None, - ok_if_none: bool = False, - conservative: bool = False): - - energy_sum = 0 - is_in_reset = False - - if excluded_modules is None: - excluded_modules = set() - - for h_id, home in self.homes.items(): - if is_in_reset: - break - for m_id, module in home.modules.items(): - if m_id in excluded_modules: - continue - if isinstance(module, EnergyHistoryMixin): - if module.in_reset: - is_in_reset = True - break - if power_adapted: - v, delta_energy = module.get_sum_energy_elec_power_adapted(to_ts=to_ts, conservative=conservative) - else: - delta_energy = 0 - v = module.sum_energy_elec - if v is not None: - energy_sum += v + delta_energy - elif ok_if_none is False: - return None, False - if is_in_reset: - return 0, is_in_reset - - return energy_sum, is_in_reset - def register_public_weather_area( self, lat_ne: str, diff --git a/tests/test_energy.py b/tests/test_energy.py index d5ac1451..f4edbee0 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -81,8 +81,6 @@ async def test_historical_data_retrieval_multi(async_account_multi): assert module.sum_energy_elec_off_peak == 11219 assert module.sum_energy_elec_peak == 31282 - assert module.sum_energy_elec == async_account_multi.get_current_energy_sum(ok_if_none = True)[0] - assert async_account_multi.get_current_energy_sum(excluded_modules={module_id}, ok_if_none = True)[0] == 0 @@ -123,11 +121,6 @@ async def test_historical_data_retrieval_multi_2(async_account_multi): assert module.sum_energy_elec_off_peak == 780 assert module.sum_energy_elec_peak == 890 - - sum, _ = async_account_multi.get_current_energy_sum(ok_if_none = True) - - assert module.sum_energy_elec == sum - async def test_disconnected_main_bridge(async_account_multi): """Test retrieval of historical measurements.""" home_id = "aaaaaaaaaaabbbbbbbbbbccc" From c26f66d5f43d4d1ee6e8ffa57a7f9817e5079db5 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 18 Apr 2024 17:57:04 +0200 Subject: [PATCH 54/97] remove global energy sum support, no needed anymore, was adding complexity --- src/pyatmo/modules/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 2f3038ed..ef654c5f 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -744,7 +744,7 @@ async def async_update_measures( # historical data for this time range LOG.debug( - "=> NO VALUES energy update %s from: %s to %s, prev_sum=%s", + "NO VALUES energy update %s from: %s to %s, prev_sum=%s", self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") else: From a756d1bc2c2a0b95344de67b912b8e469bd0316a Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 21 Apr 2024 01:09:53 +0200 Subject: [PATCH 55/97] refactor to use self.homes where it should be --- src/pyatmo/account.py | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 79034491..4ac89c49 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -23,7 +23,6 @@ from pyatmo.helpers import extract_raw_data from pyatmo.home import Home from pyatmo.modules.module import Module -from pyatmo.modules.module import EnergyHistoryMixin if TYPE_CHECKING: from pyatmo.auth import AbstractAsyncAuth @@ -87,12 +86,6 @@ def process_topology(self) -> None: self.update_supported_homes(self.support_only_homes) - def find_from_all_homes(self, home_id): - home = self.all_account_homes.get(home_id) - if home is None: - home = self.additional_public_homes.get(home_id) - return home - async def async_update_topology(self) -> int: """Retrieve topology data from /homesdata. Returns the number of performed API calls""" @@ -123,7 +116,7 @@ async def async_update_status(self, home_id: str | None = None) -> int: params={"home_id": h_id}, ) raw_data = extract_raw_data(await resp.json(), HOME) - is_correct_update = await self.all_account_homes[h_id].update(raw_data) + is_correct_update = await self.homes[h_id].update(raw_data) if not is_correct_update: all_homes_ok = False num_calls += 1 @@ -140,7 +133,7 @@ async def async_update_events(self, home_id: str) -> int: params={"home_id": home_id}, ) raw_data = extract_raw_data(await resp.json(), HOME) - await self.all_account_homes[home_id].update(raw_data) + await self.homes[home_id].update(raw_data) return 1 @@ -170,7 +163,7 @@ async def async_update_measures( ) -> int: """Retrieve measures data from /getmeasure. Returns the number of performed API calls""" - num_calls = await getattr(self.find_from_all_homes(home_id).modules[module_id], "async_update_measures")( + num_calls = await getattr(self.homes[home_id].modules[module_id], "async_update_measures")( start_time=start_time, end_time=end_time, interval=interval, @@ -263,9 +256,7 @@ async def update_devices( "home_id", self.find_home_of_device(device_data), ): - home = self.find_from_all_homes(home_id) - - if home is None: + if home_id not in self.homes: modules_data = [] for module_data in device_data.get("modules", []): module_data["home_id"] = home_id @@ -274,7 +265,7 @@ async def update_devices( modules_data.append(normalize_weather_attributes(module_data)) modules_data.append(normalize_weather_attributes(device_data)) - home = Home( + self.additional_public_homes[home_id] = Home( self.auth, raw_data={ "id": home_id, @@ -282,16 +273,14 @@ async def update_devices( "modules": modules_data, }, ) + self.update_supported_homes(self.support_only_homes) - self.additional_public_homes[home_id] = home - await home.update( + await self.homes[home_id].update( {HOME: {"modules": [normalize_weather_attributes(device_data)]}}, ) else: LOG.debug("No home %s found.", home_id) - self.update_supported_homes(self.support_only_homes) - for module_data in device_data.get("modules", []): module_data["home_id"] = home_id await self.update_devices({"devices": [module_data]}) @@ -325,15 +314,14 @@ async def update_devices( def find_home_of_device(self, device_data: dict[str, Any]) -> str | None: """Find home_id of device.""" - for home_id, home in self.all_account_homes.items(): - if device_data["_id"] in home.modules: - return home_id - - for home_id, home in self.additional_public_homes.items(): - if device_data["_id"] in home.modules: - return home_id - - return None + return next( + ( + home_id + for home_id, home in self.homes.items() + if device_data["_id"] in home.modules + ), + None, + ) ATTRIBUTES_TO_FIX = { From d2dceec472c71d493bf8710a44b57bd0c793ee4c Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 21 Apr 2024 01:18:40 +0200 Subject: [PATCH 56/97] setting schedules list back as the global list of schedules, for retro-compatibility --- src/pyatmo/home.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 21b0ab4b..0e090c35 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -40,7 +40,7 @@ class Home: name: str rooms: dict[str, Room] modules: dict[str, Module] - schedules: dict[str, ThermSchedule] # for compatibility should diseappear + schedules: dict[str, Schedule] # for compatibility should diseappear all_schedules: dict[dict[str, str, Schedule]] | {} persons: dict[str, Person] events: dict[str, Event] @@ -75,14 +75,16 @@ def _handle_schedules(self, raw_data): schedules = {} + self.schedules = {} + for s in raw_data: # strange but Energy plan are stored in schedules, we should handle this one differently sched, schedule_type = schedule_factory(home=self, raw_data=s) if schedule_type not in schedules: schedules[schedule_type] = {} schedules[schedule_type][s["id"]] = sched + self.schedules[s["id"]] = sched - self.schedules = schedules.get(SCHEDULE_TYPE_THERM, {}) self.all_schedules = schedules nrj_schedule = next(iter(schedules.get(SCHEDULE_TYPE_ELECTRICITY, {}).values()), None) From 018c04ce701e6fe26ef8e26ae2a3c0e5c949b549 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 5 May 2024 23:38:47 +0200 Subject: [PATCH 57/97] Black.... --- src/pyatmo/__init__.py | 10 +- src/pyatmo/auth.py | 4 +- src/pyatmo/modules/base_class.py | 2 +- src/pyatmo/modules/device_types.py | 2 +- src/pyatmo/modules/module.py | 278 +++++++++++++++++++---------- src/pyatmo/modules/netatmo.py | 2 +- src/pyatmo/person.py | 2 +- src/pyatmo/room.py | 2 +- src/pyatmo/schedule.py | 2 +- 9 files changed, 204 insertions(+), 100 deletions(-) diff --git a/src/pyatmo/__init__.py b/src/pyatmo/__init__.py index 37585c0b..ca3a8a89 100644 --- a/src/pyatmo/__init__.py +++ b/src/pyatmo/__init__.py @@ -3,7 +3,15 @@ from pyatmo import const, modules from pyatmo.account import AsyncAccount from pyatmo.auth import AbstractAsyncAuth -from pyatmo.exceptions import ApiError, ApiErrorThrottling, ApiHomeReachabilityError, InvalidHome, InvalidRoom, NoDevice, NoSchedule +from pyatmo.exceptions import ( + ApiError, + ApiErrorThrottling, + ApiHomeReachabilityError, + InvalidHome, + InvalidRoom, + NoDevice, + NoSchedule, +) from pyatmo.home import Home from pyatmo.modules import Module from pyatmo.modules.device_types import DeviceType diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 4e0052ed..ba67d1ae 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -2,10 +2,10 @@ from __future__ import annotations -from abc import ABC, abstractmethod import asyncio -from json import JSONDecodeError import logging +from abc import ABC, abstractmethod +from json import JSONDecodeError from typing import Any from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index a636edb0..f19612d2 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -3,10 +3,10 @@ from __future__ import annotations import bisect +import logging from abc import ABC from collections.abc import Iterable from dataclasses import dataclass -import logging from operator import itemgetter from typing import TYPE_CHECKING, Any diff --git a/src/pyatmo/modules/device_types.py b/src/pyatmo/modules/device_types.py index 6b2a27a3..2a2ba4fc 100644 --- a/src/pyatmo/modules/device_types.py +++ b/src/pyatmo/modules/device_types.py @@ -2,8 +2,8 @@ from __future__ import annotations -from enum import Enum import logging +from enum import Enum LOG = logging.getLogger(__name__) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index ef654c5f..7ea887c0 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -3,14 +3,19 @@ from __future__ import annotations import copy -from datetime import datetime, timezone, timedelta import logging +from datetime import datetime, timezone, timedelta from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError -from pyatmo.const import GETMEASURE_ENDPOINT, RawData, MeasureInterval, ENERGY_ELEC_PEAK_IDX, \ - MEASURE_INTERVAL_TO_SECONDS +from pyatmo.const import ( + GETMEASURE_ENDPOINT, + RawData, + MeasureInterval, + ENERGY_ELEC_PEAK_IDX, + MEASURE_INTERVAL_TO_SECONDS, +) from pyatmo.exceptions import ApiError from pyatmo.modules.base_class import EntityBase, NetatmoBase, Place from pyatmo.modules.device_types import DEVICE_CATEGORY_MAP, DeviceCategory, DeviceType @@ -48,7 +53,7 @@ "device_type", "features", "history_features", - "history_features_values" + "history_features_values", } @@ -616,9 +621,7 @@ def reset_measures(self): self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - - - def compute_rieman_sum(self, power_data, conservative: bool = False): + def compute_rieman_sum(self, power_data, conservative: bool = False): delta_energy = 0 if len(power_data) > 1: @@ -636,13 +639,17 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): else: d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) - d_nrj_wh = dt_h * (min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w) + d_nrj_wh = dt_h * ( + min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w + ) delta_energy += d_nrj_wh return delta_energy - def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None, conservative: bool = False): + def get_sum_energy_elec_power_adapted( + self, to_ts: int | float | None = None, conservative: bool = False + ): v = self.sum_energy_elec @@ -658,9 +665,16 @@ def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None, co from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus - if (from_ts is not None and from_ts < to_ts and isinstance(self, PowerMixin) and isinstance(self, NetatmoBase)) : + if ( + from_ts is not None + and from_ts < to_ts + and isinstance(self, PowerMixin) + and isinstance(self, NetatmoBase) + ): - power_data = self.get_history_data("power", from_ts=from_ts, to_ts=to_ts) + power_data = self.get_history_data( + "power", from_ts=from_ts, to_ts=to_ts + ) delta_energy = self.compute_rieman_sum(power_data, conservative) @@ -669,8 +683,16 @@ def get_sum_energy_elec_power_adapted(self, to_ts: int | float | None = None, co def _log_energy_error(self, start_time, end_time, msg=None, body=None): if body is None: body = "NO BODY" - LOG.debug("ENERGY collection error %s %s %s %s", msg, self.name, - datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), start_time, end_time, body) + LOG.debug( + "ENERGY collection error %s %s %s %s", + msg, + self.name, + datetime.fromtimestamp(start_time), + datetime.fromtimestamp(end_time), + start_time, + end_time, + body, + ) def update_measures_num_calls(self): @@ -709,26 +731,28 @@ async def async_update_measures( # for 1week : it will be half week ALWAYS, ie on a thursday at 12am (half day) # in fact in the case for all intervals the reported dates are "the middle" of the ranges - delta_range = MEASURE_INTERVAL_TO_SECONDS.get(interval, 0)//2 + delta_range = MEASURE_INTERVAL_TO_SECONDS.get(interval, 0) // 2 - data_points, num_calls, raw_datas, peak_off_peak_mode = await self._energy_API_calls(start_time, end_time, interval) + data_points, num_calls, raw_datas, peak_off_peak_mode = ( + await self._energy_API_calls(start_time, end_time, interval) + ) energy_schedule_vals = [] if peak_off_peak_mode: - energy_schedule_vals = await self._compute_proper_energy_schedule_offsets(start_time, - end_time, - 2 * delta_range, - raw_datas, - data_points) - - hist_good_vals = await self._get_aligned_energy_values_and_mode(start_time, - end_time, - delta_range, - energy_schedule_vals, - peak_off_peak_mode, - raw_datas, - data_points) + energy_schedule_vals = await self._compute_proper_energy_schedule_offsets( + start_time, end_time, 2 * delta_range, raw_datas, data_points + ) + + hist_good_vals = await self._get_aligned_energy_values_and_mode( + start_time, + end_time, + delta_range, + energy_schedule_vals, + peak_off_peak_mode, + raw_datas, + data_points, + ) self.historical_data = [] prev_sum_energy_elec = self.sum_energy_elec @@ -745,30 +769,37 @@ async def async_update_measures( LOG.debug( "NO VALUES energy update %s from: %s to %s, prev_sum=%s", - self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), - prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") + self.name, + datetime.fromtimestamp(start_time), + datetime.fromtimestamp(end_time), + prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING", + ) else: - await self._prepare_exported_historical_data(start_time, - end_time, - delta_range, - hist_good_vals, - prev_end_time, - prev_start_time, - prev_sum_energy_elec, - peak_off_peak_mode) + await self._prepare_exported_historical_data( + start_time, + end_time, + delta_range, + hist_good_vals, + prev_end_time, + prev_start_time, + prev_sum_energy_elec, + peak_off_peak_mode, + ) return num_calls - async def _prepare_exported_historical_data(self, - start_time, - end_time, - delta_range, - hist_good_vals, - prev_end_time, - prev_start_time, - prev_sum_energy_elec, - peak_off_peak_mode): + async def _prepare_exported_historical_data( + self, + start_time, + end_time, + delta_range, + hist_good_vals, + prev_end_time, + prev_start_time, + prev_sum_energy_elec, + peak_off_peak_mode, + ): computed_start = 0 computed_end = 0 computed_end_for_calculus = 0 @@ -804,41 +835,71 @@ async def _prepare_exported_historical_data(self, "Wh": val, "energyMode": mode, "startTimeUnix": c_start, - "endTimeUnix": c_end - + "endTimeUnix": c_end, }, ) - if prev_sum_energy_elec is not None and prev_sum_energy_elec > self.sum_energy_elec: - msg = ("ENERGY GOING DOWN %s from: %s to %s " - "computed_start: %s, computed_end: %s, " - "sum=%f prev_sum=%f prev_start: %s, prev_end %s") + if ( + prev_sum_energy_elec is not None + and prev_sum_energy_elec > self.sum_energy_elec + ): + msg = ( + "ENERGY GOING DOWN %s from: %s to %s " + "computed_start: %s, computed_end: %s, " + "sum=%f prev_sum=%f prev_start: %s, prev_end %s" + ) LOG.debug( msg, - self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), - datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, + self.name, + datetime.fromtimestamp(start_time), + datetime.fromtimestamp(end_time), + datetime.fromtimestamp(computed_start), + datetime.fromtimestamp(computed_end), + self.sum_energy_elec, prev_sum_energy_elec, - datetime.fromtimestamp(prev_start_time), datetime.fromtimestamp(prev_end_time)) + datetime.fromtimestamp(prev_start_time), + datetime.fromtimestamp(prev_end_time), + ) else: - msg = ("Success in energy update %s from: %s to %s " - "computed_start: %s, computed_end: %s , sum=%s prev_sum=%s") + msg = ( + "Success in energy update %s from: %s to %s " + "computed_start: %s, computed_end: %s , sum=%s prev_sum=%s" + ) LOG.debug( msg, - self.name, datetime.fromtimestamp(start_time), datetime.fromtimestamp(end_time), - datetime.fromtimestamp(computed_start), datetime.fromtimestamp(computed_end), self.sum_energy_elec, - prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING") - self._last_energy_from_API_end_for_power_adjustment_calculus = computed_end_for_calculus + self.name, + datetime.fromtimestamp(start_time), + datetime.fromtimestamp(end_time), + datetime.fromtimestamp(computed_start), + datetime.fromtimestamp(computed_end), + self.sum_energy_elec, + prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING", + ) + self._last_energy_from_API_end_for_power_adjustment_calculus = ( + computed_end_for_calculus + ) - async def _get_aligned_energy_values_and_mode(self, start_time, end_time, delta_range, energy_schedule_vals, - peak_off_peak_mode, raw_datas, data_points): + async def _get_aligned_energy_values_and_mode( + self, + start_time, + end_time, + delta_range, + energy_schedule_vals, + peak_off_peak_mode, + raw_datas, + data_points, + ): hist_good_vals = [] for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: try: start_lot_time = int(values_lot["beg_time"]) except Exception: - self._log_energy_error(start_time, end_time, - msg=f"beg_time missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode]) + self._log_energy_error( + start_time, + end_time, + msg=f"beg_time missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode], + ) raise ApiError( f"Energy badly formed resp beg_time missing: {raw_datas[cur_peak_or_off_peak_mode]} - " f"module: {self.name} - " @@ -848,9 +909,12 @@ async def _get_aligned_energy_values_and_mode(self, start_time, end_time, delta_ interval_sec = values_lot.get("step_time") if interval_sec is None: if len(values_lot.get("value", [])) > 1: - self._log_energy_error(start_time, end_time, - msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode]) + self._log_energy_error( + start_time, + end_time, + msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode], + ) interval_sec = 2 * delta_range else: interval_sec = int(interval_sec) @@ -864,22 +928,32 @@ async def _get_aligned_energy_values_and_mode(self, start_time, end_time, delta_ d_srt = datetime.fromtimestamp(cur_start_time) # offset from start of the day - day_origin = int(datetime(d_srt.year, d_srt.month, d_srt.day).timestamp()) + day_origin = int( + datetime(d_srt.year, d_srt.month, d_srt.day).timestamp() + ) srt_beg = cur_start_time - day_origin srt_mid = srt_beg + interval_sec // 2 # now check if srt_beg is in a schedule span of the right type - idx_limit = _get_proper_in_schedule_index(energy_schedule_vals, srt_mid) + idx_limit = _get_proper_in_schedule_index( + energy_schedule_vals, srt_mid + ) - if self.home.energy_schedule_vals[idx_limit][1] != cur_peak_or_off_peak_mode: + if ( + self.home.energy_schedule_vals[idx_limit][1] + != cur_peak_or_off_peak_mode + ): # we are NOT in a proper schedule time for this time span ... # jump to the next one... meaning it is the next day! if idx_limit == len(energy_schedule_vals) - 1: # should never append with the performed day extension above - self._log_energy_error(start_time, end_time, - msg=f"bad idx missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode]) + self._log_energy_error( + start_time, + end_time, + msg=f"bad idx missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode], + ) raise ApiError( f"Energy badly formed bad schedule idx in vals: {raw_datas[cur_peak_or_off_peak_mode]} - " @@ -888,27 +962,43 @@ async def _get_aligned_energy_values_and_mode(self, start_time, end_time, delta_ ) else: # by construction of the energy schedule the next one should be of opposite mode - if energy_schedule_vals[idx_limit + 1][1] != cur_peak_or_off_peak_mode: - self._log_energy_error(start_time, end_time, - msg=f"bad schedule {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode]) + if ( + energy_schedule_vals[idx_limit + 1][1] + != cur_peak_or_off_peak_mode + ): + self._log_energy_error( + start_time, + end_time, + msg=f"bad schedule {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode], + ) raise ApiError( f"Energy badly formed bad schedule: {raw_datas[cur_peak_or_off_peak_mode]} - " f"module: {self.name} - " f"when accessing '{data_points[cur_peak_or_off_peak_mode]}'" ) - start_time_to_get_closer = energy_schedule_vals[idx_limit + 1][0] + start_time_to_get_closer = energy_schedule_vals[ + idx_limit + 1 + ][0] diff_t = start_time_to_get_closer - srt_mid - cur_start_time = day_origin + srt_beg + (diff_t // interval_sec + 1) * interval_sec + cur_start_time = ( + day_origin + + srt_beg + + (diff_t // interval_sec + 1) * interval_sec + ) - hist_good_vals.append((cur_start_time, int(val), cur_peak_or_off_peak_mode)) + hist_good_vals.append( + (cur_start_time, int(val), cur_peak_or_off_peak_mode) + ) cur_start_time = cur_start_time + interval_sec hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) return hist_good_vals - async def _compute_proper_energy_schedule_offsets(self, start_time, end_time, interval_sec, raw_datas, data_points): + async def _compute_proper_energy_schedule_offsets( + self, start_time, end_time, interval_sec, raw_datas, data_points + ): max_interval_sec = interval_sec for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: @@ -916,9 +1006,12 @@ async def _compute_proper_energy_schedule_offsets(self, start_time, end_time, in if local_step_time is None: if len(values_lot.get("value", [])) > 1: - self._log_energy_error(start_time, end_time, - msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode]) + self._log_energy_error( + start_time, + end_time, + msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", + body=raw_datas[cur_peak_or_off_peak_mode], + ) else: local_step_time = int(local_step_time) max_interval_sec = max(max_interval_sec, local_step_time) @@ -932,7 +1025,10 @@ async def _compute_proper_energy_schedule_offsets(self, start_time, end_time, in energy_schedule_vals_next = copy.copy(self.home.energy_schedule_vals) for d in range(0, biggest_day_interval): - next_day_extend = [(offset + ((d + 1) * 24 * 3600), mode) for offset, mode in energy_schedule_vals_next] + next_day_extend = [ + (offset + ((d + 1) * 24 * 3600), mode) + for offset, mode in energy_schedule_vals_next + ] energy_schedule_vals.extend(next_day_extend) return energy_schedule_vals @@ -960,7 +1056,9 @@ async def _energy_API_calls(self, start_time, end_time, interval): rw_dt = rw_dt_f.get("body") if rw_dt is None: - self._log_energy_error(start_time, end_time, msg=f"direct from {data_point}", body=rw_dt_f) + self._log_energy_error( + start_time, end_time, msg=f"direct from {data_point}", body=rw_dt_f + ) raise ApiError( f"Energy badly formed resp: {rw_dt_f} - " f"module: {self.name} - " @@ -970,12 +1068,10 @@ async def _energy_API_calls(self, start_time, end_time, interval): num_calls += 1 raw_datas.append(rw_dt) - peak_off_peak_mode = False if len(raw_datas) > 1 and len(self.home.energy_schedule_vals) > 0: peak_off_peak_mode = True - return data_points, num_calls, raw_datas, peak_off_peak_mode diff --git a/src/pyatmo/modules/netatmo.py b/src/pyatmo/modules/netatmo.py index 047c4eb1..94decc3e 100644 --- a/src/pyatmo/modules/netatmo.py +++ b/src/pyatmo/modules/netatmo.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass import logging +from dataclasses import dataclass from typing import Any from pyatmo.const import ( diff --git a/src/pyatmo/person.py b/src/pyatmo/person.py index 8e413646..6c7392b8 100644 --- a/src/pyatmo/person.py +++ b/src/pyatmo/person.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass import logging +from dataclasses import dataclass from typing import TYPE_CHECKING from pyatmo.const import RawData diff --git a/src/pyatmo/room.py b/src/pyatmo/room.py index f6229081..e79eed6f 100644 --- a/src/pyatmo/room.py +++ b/src/pyatmo/room.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass import logging +from dataclasses import dataclass from typing import TYPE_CHECKING, Any from pyatmo.const import FROSTGUARD, HOME, MANUAL, SETROOMTHERMPOINT_ENDPOINT, RawData diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index b8f178cd..773b2db8 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass import logging +from dataclasses import dataclass from typing import TYPE_CHECKING from pyatmo.const import RawData, SCHEDULE_TYPE_THERM, SCHEDULE_TYPE_EVENT, SCHEDULE_TYPE_ELECTRICITY, \ From f161af9e9086ed1cd18ee47dc880d0d5abd19af3 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 6 May 2024 00:00:49 +0200 Subject: [PATCH 58/97] Ruff.... --- src/pyatmo/account.py | 62 +++++++++++---------- src/pyatmo/auth.py | 80 +++++++++++++-------------- src/pyatmo/const.py | 2 - src/pyatmo/home.py | 46 +++++++-------- src/pyatmo/modules/base_class.py | 13 ++--- src/pyatmo/modules/bticino.py | 2 +- src/pyatmo/modules/device_types.py | 2 +- src/pyatmo/modules/legrand.py | 6 +- src/pyatmo/modules/module.py | 89 +++++++++++++++--------------- src/pyatmo/modules/netatmo.py | 23 ++++---- src/pyatmo/room.py | 40 +++++++------- src/pyatmo/schedule.py | 10 +++- 12 files changed, 189 insertions(+), 186 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 4ac89c49..28de859d 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -17,7 +17,8 @@ GETSTATIONDATA_ENDPOINT, HOME, SETSTATE_ENDPOINT, - RawData, MeasureInterval, + MeasureInterval, + RawData, ) from pyatmo.exceptions import ApiHomeReachabilityError from pyatmo.helpers import extract_raw_data @@ -122,7 +123,8 @@ async def async_update_status(self, home_id: str | None = None) -> int: num_calls += 1 if all_homes_ok is False: - raise ApiHomeReachabilityError("No Home update could be performed, all modules unreachable and not updated", ) + raise ApiHomeReachabilityError( + "No Home update could be performed, all modules unreachable and not updated", ) return num_calls @@ -153,13 +155,13 @@ async def async_update_air_care(self) -> int: return 1 async def async_update_measures( - self, - home_id: str, - module_id: str, - start_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, - end_time: int | None = None + self, + home_id: str, + module_id: str, + start_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7, + end_time: int | None = None ) -> int: """Retrieve measures data from /getmeasure. Returns the number of performed API calls""" @@ -172,15 +174,15 @@ async def async_update_measures( return num_calls def register_public_weather_area( - self, - lat_ne: str, - lon_ne: str, - lat_sw: str, - lon_sw: str, - required_data_type: str | None = None, - filtering: bool = False, - *, - area_id: str = str(uuid4()), + self, + lat_ne: str, + lon_ne: str, + lat_sw: str, + lon_sw: str, + required_data_type: str | None = None, + filtering: bool = False, + *, + area_id: str = str(uuid4()), ) -> str: """Register public weather area to monitor.""" @@ -215,11 +217,11 @@ async def async_update_public_weather(self, area_id: str) -> int: return 1 async def _async_update_data( - self, - endpoint: str, - params: dict[str, Any] | None = None, - tag: str = "devices", - area_id: str | None = None, + self, + endpoint: str, + params: dict[str, Any] | None = None, + tag: str = "devices", + area_id: str | None = None, ) -> None: """Retrieve status data from .""" resp = await self.auth.async_post_api_request(endpoint=endpoint, params=params) @@ -246,15 +248,15 @@ async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: return 1 async def update_devices( - self, - raw_data: RawData, - area_id: str | None = None, + self, + raw_data: RawData, + area_id: str | None = None, ) -> None: """Update device states.""" for device_data in raw_data.get("devices", {}): if home_id := device_data.get( - "home_id", - self.find_home_of_device(device_data), + "home_id", + self.find_home_of_device(device_data), ): if home_id not in self.homes: modules_data = [] @@ -286,8 +288,8 @@ async def update_devices( await self.update_devices({"devices": [module_data]}) if ( - device_data["type"] == "NHC" - or self.find_home_of_device(device_data) is None + device_data["type"] == "NHC" + or self.find_home_of_device(device_data) is None ): device_data["name"] = device_data.get( "station_name", diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index ba67d1ae..f37c774a 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -26,9 +26,9 @@ class AbstractAsyncAuth(ABC): """Abstract class to make authenticated requests.""" def __init__( - self, - websession: ClientSession, - base_url: str = DEFAULT_BASE_URL, + self, + websession: ClientSession, + base_url: str = DEFAULT_BASE_URL, ) -> None: """Initialize the auth.""" @@ -40,11 +40,11 @@ async def async_get_access_token(self) -> str: """Return a valid access token.""" async def async_get_image( - self, - endpoint: str, - base_url: str | None = None, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + endpoint: str, + base_url: str | None = None, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> bytes: """Wrap async get requests.""" @@ -58,10 +58,10 @@ async def async_get_image( url = (base_url or self.base_url) + endpoint async with self.websession.get( - url, - **req_args, # type: ignore - headers=headers, - timeout=timeout, + url, + **req_args, # type: ignore + headers=headers, + timeout=timeout, ) as resp: resp_content = await resp.read() @@ -75,11 +75,11 @@ async def async_get_image( ) async def async_post_api_request( - self, - endpoint: str, - base_url: str | None = None, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + endpoint: str, + base_url: str | None = None, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> ClientResponse: """Wrap async post requests.""" @@ -90,11 +90,11 @@ async def async_post_api_request( ) async def async_get_api_request( - self, - endpoint: str, - base_url: str | None = None, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + endpoint: str, + base_url: str | None = None, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> ClientResponse: """Wrap async post requests.""" @@ -105,10 +105,10 @@ async def async_get_api_request( ) async def async_get_request( - self, - url: str, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + url: str, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> ClientResponse: """Wrap async post requests.""" @@ -118,18 +118,18 @@ async def async_get_request( req_args = self.prepare_request_get_arguments(params) async with self.websession.get( - url, - **req_args, - headers=headers, - timeout=timeout, + url, + **req_args, + headers=headers, + timeout=timeout, ) as resp: return await self.process_response(resp, url) async def async_post_request( - self, - url: str, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + url: str, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> ClientResponse: """Wrap async post requests.""" @@ -139,10 +139,10 @@ async def async_post_request( req_args = self.prepare_request_arguments(params) async with self.websession.post( - url, - **req_args, - headers=headers, - timeout=timeout, + url, + **req_args, + headers=headers, + timeout=timeout, ) as resp: return await self.process_response(resp, url) @@ -189,10 +189,10 @@ async def handle_error_response(self, resp, resp_status, url): message = (f"{resp_status} - {ERRORS.get(resp_status, '')} - {resp_json['error']['message']} " f"({resp_json['error']['code']}) when accessing '{url}'") - if resp_status == 403 and resp_json['error']['code'] == 26: + if resp_status == 403 and resp_json["error"]["code"] == 26: raise ApiErrorThrottling(message, ) else: - raise ApiError(message,) + raise ApiError(message, ) except (JSONDecodeError, ContentTypeError) as exc: raise ApiError( diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index feeb50ab..de21662e 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -90,7 +90,6 @@ SCHEDULES = "schedules" EVENTS = "events" - STATION_TEMPERATURE_TYPE = "temperature" STATION_PRESSURE_TYPE = "pressure" STATION_HUMIDITY_TYPE = "humidity" @@ -110,7 +109,6 @@ SCHEDULE_TYPE_ELECTRICITY = "electricity" SCHEDULE_TYPE_COOLING = "cooling" - ENERGY_ELEC_PEAK_IDX = 0 ENERGY_ELEC_OFF_IDX = 1 diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 0e090c35..acb7a307 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -9,7 +9,11 @@ from pyatmo import modules from pyatmo.const import ( + ENERGY_ELEC_OFF_IDX, + ENERGY_ELEC_PEAK_IDX, EVENTS, + SCHEDULE_TYPE_ELECTRICITY, + SCHEDULE_TYPE_THERM, SCHEDULES, SETPERSONSAWAY_ENDPOINT, SETPERSONSHOME_ENDPOINT, @@ -17,14 +21,15 @@ SETTHERMMODE_ENDPOINT, SWITCHHOMESCHEDULE_ENDPOINT, SYNCHOMESCHEDULE_ENDPOINT, - RawData, SCHEDULE_TYPE_THERM, SCHEDULE_TYPE_ELECTRICITY, MeasureType, ENERGY_ELEC_PEAK_IDX, ENERGY_ELEC_OFF_IDX, + MeasureType, + RawData, ) from pyatmo.event import Event from pyatmo.exceptions import InvalidSchedule, InvalidState, NoSchedule from pyatmo.modules import Module from pyatmo.person import Person from pyatmo.room import Room -from pyatmo.schedule import Schedule, schedule_factory, ThermSchedule +from pyatmo.schedule import Schedule, ThermSchedule, schedule_factory if TYPE_CHECKING: from pyatmo.auth import AbstractAsyncAuth @@ -114,7 +119,7 @@ def _handle_schedules(self, raw_data): # timetable are daily for electricity type, and sorted from begining to end for t in timetable: - time = t.m_offset*60 # m_offset is in minute from the begininng of the day + time = t.m_offset * 60 # m_offset is in minute from the begininng of the day if len(self.energy_schedule_vals) == 0: time = 0 @@ -220,16 +225,11 @@ async def update(self, raw_data: RawData) -> bool: ], ) - if num_errors > 0 and has_one_module_reachable is False and has_an_update is False: + if num_errors > 0 and has_one_module_reachable is False and has_an_update is False: return False return True - - - - - def get_selected_schedule(self, schedule_type: str = None) -> Schedule | None: """Return selected schedule for given home.""" if schedule_type is None: @@ -273,10 +273,10 @@ def get_away_temp(self) -> float | None: return schedule.away_temp async def async_set_thermmode( - self, - mode: str, - end_time: int | None = None, - schedule_id: str | None = None, + self, + mode: str, + end_time: int | None = None, + schedule_id: str | None = None, ) -> bool: """Set thermotat mode.""" if schedule_id is not None and not self.is_valid_schedule(schedule_id): @@ -327,8 +327,8 @@ async def async_set_state(self, data: dict[str, Any]) -> bool: return (await resp.json()).get("status") == "ok" async def async_set_persons_home( - self, - person_ids: list[str] | None = None, + self, + person_ids: list[str] | None = None, ) -> ClientResponse: """Mark persons as home.""" post_params: dict[str, Any] = {"home_id": self.entity_id} @@ -340,8 +340,8 @@ async def async_set_persons_home( ) async def async_set_persons_away( - self, - person_id: str | None = None, + self, + person_id: str | None = None, ) -> ClientResponse: """Mark a person as away or set the whole home to being empty.""" @@ -354,9 +354,9 @@ async def async_set_persons_away( ) async def async_set_schedule_temperatures( - self, - zone_id: int, - temps: dict[str, int], + self, + zone_id: int, + temps: dict[str, int], ) -> None: """Set the scheduled room temperature for the given schedule ID.""" @@ -404,9 +404,9 @@ async def async_set_schedule_temperatures( await self.async_sync_schedule(selected_schedule.entity_id, schedule) async def async_sync_schedule( - self, - schedule_id: str, - schedule: dict[str, Any], + self, + schedule_id: str, + schedule: dict[str, Any], ) -> None: """Modify an existing schedule.""" if not is_valid_schedule(schedule): diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index f19612d2..0db39b46 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -21,7 +21,6 @@ LOG = logging.getLogger(__name__) - NETATMO_ATTRIBUTES_MAP = { "entity_id": lambda x, y: x.get("id", y), "modules": lambda x, y: x.get("modules_bridged", y), @@ -60,7 +59,7 @@ class EntityBase: # 2 days of dynamic historical data stored -MAX_HISTORY_TIME_S = 24*2*3600 +MAX_HISTORY_TIME_S = 24 * 2 * 3600 class NetatmoBase(EntityBase, ABC): @@ -80,9 +79,9 @@ def update_topology(self, raw_data: RawData) -> None: self._update_attributes(raw_data) if ( - self.bridge - and self.bridge in self.home.modules - and getattr(self, "device_category") == "weather" + self.bridge + and self.bridge in self.home.modules + and getattr(self, "device_category") == "weather" ): self.name = update_name(self.name, self.home.modules[self.bridge].name) @@ -169,8 +168,8 @@ class Place: location: Location | None def __init__( - self, - data: dict[str, Any], + self, + data: dict[str, Any], ) -> None: """Initialize self.""" diff --git a/src/pyatmo/modules/bticino.py b/src/pyatmo/modules/bticino.py index 796f4e92..7e38af69 100644 --- a/src/pyatmo/modules/bticino.py +++ b/src/pyatmo/modules/bticino.py @@ -4,7 +4,7 @@ import logging -from pyatmo.modules.module import Dimmer, Module, Shutter, Switch, OffloadMixin +from pyatmo.modules.module import Dimmer, Module, OffloadMixin, Shutter, Switch LOG = logging.getLogger(__name__) diff --git a/src/pyatmo/modules/device_types.py b/src/pyatmo/modules/device_types.py index 2a2ba4fc..7c0f0e12 100644 --- a/src/pyatmo/modules/device_types.py +++ b/src/pyatmo/modules/device_types.py @@ -7,6 +7,7 @@ LOG = logging.getLogger(__name__) + # pylint: disable=W0613 @@ -205,7 +206,6 @@ class DeviceCategory(str, Enum): DeviceType.NLLF: DeviceCategory.fan, } - DEVICE_DESCRIPTION_MAP: dict[DeviceType, tuple[str, str]] = { # Netatmo Climate/Energy DeviceType.NAPlug: ("Netatmo", "Smart Thermostat Gateway"), diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 992105f4..d120ee0c 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -7,10 +7,11 @@ from pyatmo.modules.module import ( BatteryMixin, ContactorMixin, + DimmableMixin, Dimmer, + EnergyHistoryMixin, Fan, FirmwareMixin, - EnergyHistoryMixin, Module, OffloadMixin, PowerMixin, @@ -18,11 +19,12 @@ ShutterMixin, Switch, SwitchMixin, - WifiMixin, DimmableMixin, + WifiMixin, ) LOG = logging.getLogger(__name__) + # pylint: disable=R0901 diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 7ea887c0..b1913314 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -4,17 +4,17 @@ import copy import logging -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError from pyatmo.const import ( - GETMEASURE_ENDPOINT, - RawData, - MeasureInterval, ENERGY_ELEC_PEAK_IDX, + GETMEASURE_ENDPOINT, MEASURE_INTERVAL_TO_SECONDS, + MeasureInterval, + RawData, ) from pyatmo.exceptions import ApiError from pyatmo.modules.base_class import EntityBase, NetatmoBase, Place @@ -24,7 +24,6 @@ from pyatmo.event import Event from pyatmo.home import Home - import bisect from operator import itemgetter from time import time @@ -640,7 +639,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) d_nrj_wh = dt_h * ( - min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w + min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w ) delta_energy += d_nrj_wh @@ -648,7 +647,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): return delta_energy def get_sum_energy_elec_power_adapted( - self, to_ts: int | float | None = None, conservative: bool = False + self, to_ts: int | float | None = None, conservative: bool = False ): v = self.sum_energy_elec @@ -666,12 +665,11 @@ def get_sum_energy_elec_power_adapted( from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus if ( - from_ts is not None - and from_ts < to_ts - and isinstance(self, PowerMixin) - and isinstance(self, NetatmoBase) + from_ts is not None + and from_ts < to_ts + and isinstance(self, PowerMixin) + and isinstance(self, NetatmoBase) ): - power_data = self.get_history_data( "power", from_ts=from_ts, to_ts=to_ts ) @@ -702,11 +700,11 @@ def update_measures_num_calls(self): return len(self.home.energy_endpoints) async def async_update_measures( - self, - start_time: int | None = None, - end_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, + self, + start_time: int | None = None, + end_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7, ) -> int | None: """Update historical data.""" @@ -790,15 +788,15 @@ async def async_update_measures( return num_calls async def _prepare_exported_historical_data( - self, - start_time, - end_time, - delta_range, - hist_good_vals, - prev_end_time, - prev_start_time, - prev_sum_energy_elec, - peak_off_peak_mode, + self, + start_time, + end_time, + delta_range, + hist_good_vals, + prev_end_time, + prev_start_time, + prev_sum_energy_elec, + peak_off_peak_mode, ): computed_start = 0 computed_end = 0 @@ -839,8 +837,8 @@ async def _prepare_exported_historical_data( }, ) if ( - prev_sum_energy_elec is not None - and prev_sum_energy_elec > self.sum_energy_elec + prev_sum_energy_elec is not None + and prev_sum_energy_elec > self.sum_energy_elec ): msg = ( "ENERGY GOING DOWN %s from: %s to %s " @@ -879,14 +877,14 @@ async def _prepare_exported_historical_data( ) async def _get_aligned_energy_values_and_mode( - self, - start_time, - end_time, - delta_range, - energy_schedule_vals, - peak_off_peak_mode, - raw_datas, - data_points, + self, + start_time, + end_time, + delta_range, + energy_schedule_vals, + peak_off_peak_mode, + raw_datas, + data_points, ): hist_good_vals = [] for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): @@ -940,8 +938,8 @@ async def _get_aligned_energy_values_and_mode( ) if ( - self.home.energy_schedule_vals[idx_limit][1] - != cur_peak_or_off_peak_mode + self.home.energy_schedule_vals[idx_limit][1] + != cur_peak_or_off_peak_mode ): # we are NOT in a proper schedule time for this time span ... @@ -963,8 +961,8 @@ async def _get_aligned_energy_values_and_mode( else: # by construction of the energy schedule the next one should be of opposite mode if ( - energy_schedule_vals[idx_limit + 1][1] - != cur_peak_or_off_peak_mode + energy_schedule_vals[idx_limit + 1][1] + != cur_peak_or_off_peak_mode ): self._log_energy_error( start_time, @@ -980,12 +978,12 @@ async def _get_aligned_energy_values_and_mode( start_time_to_get_closer = energy_schedule_vals[ idx_limit + 1 - ][0] + ][0] diff_t = start_time_to_get_closer - srt_mid cur_start_time = ( - day_origin - + srt_beg - + (diff_t // interval_sec + 1) * interval_sec + day_origin + + srt_beg + + (diff_t // interval_sec + 1) * interval_sec ) hist_good_vals.append( @@ -997,7 +995,7 @@ async def _get_aligned_energy_values_and_mode( return hist_good_vals async def _compute_proper_energy_schedule_offsets( - self, start_time, end_time, interval_sec, raw_datas, data_points + self, start_time, end_time, interval_sec, raw_datas, data_points ): max_interval_sec = interval_sec for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): @@ -1169,5 +1167,4 @@ class Fan(FirmwareMixin, FanSpeedMixin, PowerMixin, Module): ... - # pylint: enable=too-many-ancestors diff --git a/src/pyatmo/modules/netatmo.py b/src/pyatmo/modules/netatmo.py index 94decc3e..f8bc4441 100644 --- a/src/pyatmo/modules/netatmo.py +++ b/src/pyatmo/modules/netatmo.py @@ -43,6 +43,7 @@ LOG = logging.getLogger(__name__) + # pylint: disable=R0901 @@ -224,13 +225,13 @@ class PublicWeatherArea: modules: list[dict[str, Any]] def __init__( - self, - lat_ne: str, - lon_ne: str, - lat_sw: str, - lon_sw: str, - required_data_type: str | None = None, - filtering: bool = False, + self, + lat_ne: str, + lon_ne: str, + lat_sw: str, + lon_sw: str, + required_data_type: str | None = None, + filtering: bool = False, ) -> None: """Initialize self.""" @@ -302,10 +303,10 @@ def get_latest_station_measures(self, data_type: str) -> dict[str, Any]: for station in self.modules: for module in station["measures"].values(): if ( - "type" in module - and data_type in module["type"] - and "res" in module - and module["res"] + "type" in module + and data_type in module["type"] + and "res" in module + and module["res"] ): measure_index = module["type"].index(data_type) latest_timestamp = sorted(module["res"], reverse=True)[0] diff --git a/src/pyatmo/room.py b/src/pyatmo/room.py index e79eed6f..f21fbacb 100644 --- a/src/pyatmo/room.py +++ b/src/pyatmo/room.py @@ -39,10 +39,10 @@ class Room(NetatmoBase): therm_setpoint_end_time: int | None = None def __init__( - self, - home: Home, - room: dict[str, Any], - all_modules: dict[str, Module], + self, + home: Home, + room: dict[str, Any], + all_modules: dict[str, Module], ) -> None: """Initialize a Netatmo room instance.""" @@ -99,9 +99,9 @@ def update(self, raw_data: RawData) -> None: self.therm_setpoint_end_time = raw_data.get("therm_setpoint_end_time") async def async_therm_manual( - self, - temp: float | None = None, - end_time: int | None = None, + self, + temp: float | None = None, + end_time: int | None = None, ) -> None: """Set room temperature set point to manual.""" @@ -118,17 +118,17 @@ async def async_therm_frostguard(self, end_time: int | None = None) -> None: await self.async_therm_set(FROSTGUARD, end_time=end_time) async def async_therm_set( - self, - mode: str, - temp: float | None = None, - end_time: int | None = None, + self, + mode: str, + temp: float | None = None, + end_time: int | None = None, ) -> None: """Set room temperature set point.""" mode = MODE_MAP.get(mode, mode) if "NATherm1" in self.device_types or ( - "NRV" in self.device_types and not self.home.has_otm() + "NRV" in self.device_types and not self.home.has_otm() ): await self._async_set_thermpoint(mode, temp, end_time) @@ -136,10 +136,10 @@ async def async_therm_set( await self._async_therm_set(mode, temp, end_time) async def _async_therm_set( - self, - mode: str, - temp: float | None = None, - end_time: int | None = None, + self, + mode: str, + temp: float | None = None, + end_time: int | None = None, ) -> bool: """Set room temperature set point (OTM).""" @@ -161,10 +161,10 @@ async def _async_therm_set( return await self.home.async_set_state(json_therm_set) async def _async_set_thermpoint( - self, - mode: str, - temp: float | None = None, - end_time: int | None = None, + self, + mode: str, + temp: float | None = None, + end_time: int | None = None, ) -> None: """Set room temperature set point (NRV, NATherm1).""" diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index 773b2db8..54534c6f 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -6,8 +6,13 @@ from dataclasses import dataclass from typing import TYPE_CHECKING -from pyatmo.const import RawData, SCHEDULE_TYPE_THERM, SCHEDULE_TYPE_EVENT, SCHEDULE_TYPE_ELECTRICITY, \ - SCHEDULE_TYPE_COOLING +from pyatmo.const import ( + SCHEDULE_TYPE_COOLING, + SCHEDULE_TYPE_ELECTRICITY, + SCHEDULE_TYPE_EVENT, + SCHEDULE_TYPE_THERM, + RawData, +) from pyatmo.modules.base_class import NetatmoBase from pyatmo.room import Room @@ -146,7 +151,6 @@ def __init__(self, home: Home, raw_data: RawData) -> None: class ModuleSchedule(NetatmoBase): - on: bool | None target_position: int | None fan_speed: int | None From ca51a008c2d1938711b16af90b2ee3d6afddf7cc Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 6 May 2024 00:27:45 +0200 Subject: [PATCH 59/97] Ruff.... --- src/pyatmo/account.py | 19 ++++++++++--------- src/pyatmo/auth.py | 5 +++-- src/pyatmo/home.py | 2 ++ src/pyatmo/modules/base_class.py | 5 +++-- src/pyatmo/modules/device_types.py | 2 +- src/pyatmo/modules/legrand.py | 4 ++-- src/pyatmo/modules/module.py | 15 +++++++++------ src/pyatmo/modules/netatmo.py | 2 +- src/pyatmo/person.py | 2 +- src/pyatmo/room.py | 2 +- src/pyatmo/schedule.py | 12 ++++++++++-- 11 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 28de859d..afe6dd04 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -59,6 +59,7 @@ def __repr__(self) -> str: ) def update_supported_homes(self, support_only_homes: list | None = None): + """Update the exposed/supported homes.""" if support_only_homes is None or len(support_only_homes) == 0: self.homes = copy.copy(self.all_account_homes) @@ -72,7 +73,7 @@ def update_supported_homes(self, support_only_homes: list | None = None): if len(self.homes) == 0: self.homes = copy.copy(self.all_account_homes) - self.support_only_homes = [h_id for h_id in self.homes] + self.support_only_homes = list(self.homes) self.homes.update(self.additional_public_homes) @@ -88,7 +89,7 @@ def process_topology(self) -> None: self.update_supported_homes(self.support_only_homes) async def async_update_topology(self) -> int: - """Retrieve topology data from /homesdata. Returns the number of performed API calls""" + """Retrieve topology data from /homesdata. Returns the number of performed API calls.""" resp = await self.auth.async_post_api_request( endpoint=GETHOMESDATA_ENDPOINT, @@ -102,7 +103,7 @@ async def async_update_topology(self) -> int: return 1 async def async_update_status(self, home_id: str | None = None) -> int: - """Retrieve status data from /homestatus. Returns the number of performed API calls""" + """Retrieve status data from /homestatus. Returns the number of performed API calls.""" if home_id is None: self.update_supported_homes(self.support_only_homes) @@ -129,7 +130,7 @@ async def async_update_status(self, home_id: str | None = None) -> int: return num_calls async def async_update_events(self, home_id: str) -> int: - """Retrieve events from /getevents. Returns the number of performed API calls""" + """Retrieve events from /getevents. Returns the number of performed API calls.""" resp = await self.auth.async_post_api_request( endpoint=GETEVENTS_ENDPOINT, params={"home_id": home_id}, @@ -140,7 +141,7 @@ async def async_update_events(self, home_id: str) -> int: return 1 async def async_update_weather_stations(self) -> int: - """Retrieve status data from /getstationsdata. Returns the number of performed API calls""" + """Retrieve status data from /getstationsdata. Returns the number of performed API calls.""" params = {"get_favorites": ("true" if self.favorite_stations else "false")} await self._async_update_data( GETSTATIONDATA_ENDPOINT, @@ -149,7 +150,7 @@ async def async_update_weather_stations(self) -> int: return 1 async def async_update_air_care(self) -> int: - """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls""" + """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls.""" await self._async_update_data(GETHOMECOACHDATA_ENDPOINT) return 1 @@ -163,7 +164,7 @@ async def async_update_measures( days: int = 7, end_time: int | None = None ) -> int: - """Retrieve measures data from /getmeasure. Returns the number of performed API calls""" + """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" num_calls = await getattr(self.homes[home_id].modules[module_id], "async_update_measures")( start_time=start_time, @@ -197,7 +198,7 @@ def register_public_weather_area( return area_id async def async_update_public_weather(self, area_id: str) -> int: - """Retrieve status data from /getpublicdata. Returns the number of performed API calls""" + """Retrieve status data from /getpublicdata. Returns the number of performed API calls.""" params = { "lat_ne": self.public_weather_areas[area_id].location.lat_ne, "lon_ne": self.public_weather_areas[area_id].location.lon_ne, @@ -229,7 +230,7 @@ async def _async_update_data( await self.update_devices(raw_data, area_id) async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: - """Modify device state by passing JSON specific to the device. Returns the number of performed API calls""" + """Modify device state by passing JSON specific to the device. Returns the number of performed API calls.""" LOG.debug("Setting state: %s", data) post_params = { diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index f37c774a..2f83b601 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -2,10 +2,10 @@ from __future__ import annotations -import asyncio -import logging from abc import ABC, abstractmethod +import asyncio from json import JSONDecodeError +import logging from typing import Any from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError @@ -168,6 +168,7 @@ def prepare_request_arguments(self, params): return req_args def prepare_request_get_arguments(self, params): + """Prepare get request arguments.""" return params async def process_response(self, resp, url): diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index acb7a307..55eeced9 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -243,6 +243,8 @@ def get_selected_schedule(self, schedule_type: str = None) -> Schedule | None: ) def get_selected_temperature_schedule(self) -> ThermSchedule | None: + """Return selected temperature schedule for given home.""" + return self.get_selected_schedule(schedule_type=SCHEDULE_TYPE_THERM) def is_valid_schedule(self, schedule_id: str) -> bool: diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 0db39b46..b0950ab8 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -2,11 +2,11 @@ from __future__ import annotations -import bisect -import logging from abc import ABC +import bisect from collections.abc import Iterable from dataclasses import dataclass +import logging from operator import itemgetter from typing import TYPE_CHECKING, Any @@ -121,6 +121,7 @@ def _update_attributes(self, raw_data: RawData) -> None: hist_f.pop(0) def get_history_data(self, feature: str, from_ts: int, to_ts: int | None = None): + """Retrieve historical data.""" hist_f = self.history_features_values.get(feature, []) diff --git a/src/pyatmo/modules/device_types.py b/src/pyatmo/modules/device_types.py index 7c0f0e12..e5fbe849 100644 --- a/src/pyatmo/modules/device_types.py +++ b/src/pyatmo/modules/device_types.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from enum import Enum +import logging LOG = logging.getLogger(__name__) diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index d120ee0c..657c83cf 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -33,7 +33,7 @@ class NLG(FirmwareMixin, OffloadMixin, WifiMixin, Module): class NLT(DimmableMixin, FirmwareMixin, BatteryMixin, SwitchMixin, Module): - """Legrand global remote control...but also wireless switch, like NLD""" + """Legrand global remote control...but also wireless switch, like NLD.""" class NLP(Switch, OffloadMixin): @@ -77,7 +77,7 @@ class NLIS(Switch): class NLD(DimmableMixin, FirmwareMixin, BatteryMixin, SwitchMixin, Module): - """Legrand Double On/Off dimmer remote. Wireless 2 button switch light""" + """Legrand Double On/Off dimmer remote. Wireless 2 button switch light.""" class NLL(Switch, WifiMixin): diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index b1913314..2bd82a4c 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -3,8 +3,8 @@ from __future__ import annotations import copy -import logging from datetime import datetime, timedelta, timezone +import logging from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError @@ -613,6 +613,7 @@ def __init__(self, home: Home, module: ModuleT): self.in_reset: bool | False = False def reset_measures(self): + """Reset energy measures.""" self.in_reset = True self.historical_data = [] self._last_energy_from_API_end_for_power_adjustment_calculus = None @@ -621,6 +622,7 @@ def reset_measures(self): self.sum_energy_elec_off_peak = 0 def compute_rieman_sum(self, power_data, conservative: bool = False): + """Compute energy from power with a rieman sum.""" delta_energy = 0 if len(power_data) > 1: @@ -649,7 +651,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): def get_sum_energy_elec_power_adapted( self, to_ts: int | float | None = None, conservative: bool = False ): - + """Compute proper energy value with adaptation from power.""" v = self.sum_energy_elec if v is None: @@ -673,8 +675,8 @@ def get_sum_energy_elec_power_adapted( power_data = self.get_history_data( "power", from_ts=from_ts, to_ts=to_ts ) - - delta_energy = self.compute_rieman_sum(power_data, conservative) + if isinstance(self, EnergyHistoryMixin): #well to please the linter.... + delta_energy = self.compute_rieman_sum(power_data, conservative) return v, delta_energy @@ -682,7 +684,7 @@ def _log_energy_error(self, start_time, end_time, msg=None, body=None): if body is None: body = "NO BODY" LOG.debug( - "ENERGY collection error %s %s %s %s", + "ENERGY collection error %s %s %s %s %s %s %s", msg, self.name, datetime.fromtimestamp(start_time), @@ -693,6 +695,7 @@ def _log_energy_error(self, start_time, end_time, msg=None, body=None): ) def update_measures_num_calls(self): + """Get number of possible endpoint calls.""" if not self.home.energy_endpoints: return 1 @@ -902,7 +905,7 @@ async def _get_aligned_energy_values_and_mode( f"Energy badly formed resp beg_time missing: {raw_datas[cur_peak_or_off_peak_mode]} - " f"module: {self.name} - " f"when accessing '{data_points[cur_peak_or_off_peak_mode]}'" - ) + ) from None interval_sec = values_lot.get("step_time") if interval_sec is None: diff --git a/src/pyatmo/modules/netatmo.py b/src/pyatmo/modules/netatmo.py index f8bc4441..98d4be90 100644 --- a/src/pyatmo/modules/netatmo.py +++ b/src/pyatmo/modules/netatmo.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import Any from pyatmo.const import ( diff --git a/src/pyatmo/person.py b/src/pyatmo/person.py index 6c7392b8..8e413646 100644 --- a/src/pyatmo/person.py +++ b/src/pyatmo/person.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING from pyatmo.const import RawData diff --git a/src/pyatmo/room.py b/src/pyatmo/room.py index f21fbacb..337049f7 100644 --- a/src/pyatmo/room.py +++ b/src/pyatmo/room.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING, Any from pyatmo.const import FROSTGUARD, HOME, MANUAL, SETROOMTHERMPOINT_ENDPOINT, RawData diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index 54534c6f..fdbd8196 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING from pyatmo.const import ( @@ -65,6 +65,7 @@ class ThermSchedule(ScheduleWithRealZones): hg_temp: float | None def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize ThermSchedule.""" super().__init__(home, raw_data) self.hg_temp = raw_data.get("hg_temp") self.away_temp = raw_data.get("away_temp") @@ -78,6 +79,7 @@ class CoolingSchedule(ThermSchedule): hg_temp: float | None def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize CoolingSchedule.""" super().__init__(home, raw_data) self.cooling_away_temp = self.away_temp = raw_data.get("cooling_away_temp", self.away_temp) @@ -93,9 +95,10 @@ class ElectricitySchedule(Schedule): zones: list[ZoneElectricity] def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize ElectricitySchedule.""" super().__init__(home, raw_data) self.tariff = raw_data.get("tariff", "custom") - # Tariff option (basic = always the same price, peak_and_off_peak = peak & off peak hours) + # Tariff option (basic = always the same price, peak_and_off_peak = peak & offpeak hours) self.tariff_option = raw_data.get("tariff_option", "basic") self.power_threshold = raw_data.get("power_threshold", 6) self.contract_power_unit = raw_data.get("power_threshold", "kVA") @@ -110,6 +113,7 @@ class EventSchedule(Schedule): timetable_sunset: list[TimetableEventEntry] def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize EventSchedule.""" super().__init__(home, raw_data) self.timetable_sunrise = [ TimetableEventEntry(home, r) for r in raw_data.get("timetable_sunrise", []) @@ -151,6 +155,8 @@ def __init__(self, home: Home, raw_data: RawData) -> None: class ModuleSchedule(NetatmoBase): + """Class to represent a Netatmo schedule.""" + on: bool | None target_position: int | None fan_speed: int | None @@ -205,6 +211,8 @@ def __init__(self, home: Home, raw_data: RawData) -> None: def schedule_factory(home: Home, raw_data: RawData) -> (Schedule, str): + """Create proper schedules.""" + schedule_type = raw_data.get("type", "custom") cls = {SCHEDULE_TYPE_THERM: ThermSchedule, SCHEDULE_TYPE_EVENT: EventSchedule, From 78dcd9536c089bb387d624d14170507ef0e3575f Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 6 May 2024 14:36:38 +0200 Subject: [PATCH 60/97] Ruff.... --- tests/test_energy.py | 66 +++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/tests/test_energy.py b/tests/test_energy.py index f4edbee0..f5ec5dd8 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -1,22 +1,16 @@ """Define tests for energy module.""" -from pyatmo import DeviceType, ApiHomeReachabilityError -import pytest - -import time_machine import datetime as dt - -from pyatmo.const import MeasureInterval -from pyatmo.modules.module import EnergyHistoryMixin - - import json from unittest.mock import AsyncMock, patch -from pyatmo import DeviceType, NoSchedule +from pyatmo import ApiHomeReachabilityError, DeviceType +from pyatmo.const import MeasureInterval +from pyatmo.modules.module import EnergyHistoryMixin import pytest +import time_machine -from tests.common import MockResponse, fake_post_request +from tests.common import MockResponse # pylint: disable=F6401 @@ -30,6 +24,7 @@ async def test_async_energy_NLPC(async_home): # pylint: disable=invalid-name assert module.device_type == DeviceType.NLPC assert module.power == 476 + @time_machine.travel(dt.datetime(2022, 2, 12, 7, 59, 49)) @pytest.mark.asyncio async def test_historical_data_retrieval(async_account): @@ -44,13 +39,16 @@ async def test_historical_data_retrieval(async_account): assert module.device_type == DeviceType.NLPC await async_account.async_update_measures(home_id=home_id, module_id=module_id) - #changed teh reference here as start and stop data was not calculated in the spirit of the netatmo api where their time data is in the fact representing the "middle" of the range and not the begining - assert module.historical_data[0] == {'Wh': 197, 'duration': 60, 'endTime': '2022-02-05T08:59:49Z', 'endTimeUnix': 1644051589, 'energyMode': 'standard', 'startTime': '2022-02-05T07:59:50Z', 'startTimeUnix': 1644047989} - assert module.historical_data[-1] == {'Wh': 259, 'duration': 60, 'endTime': '2022-02-12T07:59:49Z', 'endTimeUnix': 1644652789, 'energyMode': 'standard', 'startTime': '2022-02-12T06:59:50Z', 'startTimeUnix': 1644649189} + # changed teh reference here as start and stop data was not calculated in the spirit of the netatmo api where their time data is in the fact representing the "middle" of the range and not the begining + assert module.historical_data[0] == {"Wh": 197, "duration": 60, "endTime": "2022-02-05T08:59:49Z", + "endTimeUnix": 1644051589, "energyMode": "standard", + "startTime": "2022-02-05T07:59:50Z", "startTimeUnix": 1644047989} + assert module.historical_data[-1] == {"Wh": 259, "duration": 60, "endTime": "2022-02-12T07:59:49Z", + "endTimeUnix": 1644652789, "energyMode": "standard", + "startTime": "2022-02-12T06:59:50Z", "startTimeUnix": 1644649189} assert len(module.historical_data) == 168 - async def test_historical_data_retrieval_multi(async_account_multi): """Test retrieval of historical measurements.""" home_id = "aaaaaaaaaaabbbbbbbbbbccc" @@ -73,8 +71,12 @@ async def test_historical_data_retrieval_multi(async_account_multi): ) assert isinstance(module, EnergyHistoryMixin) - assert module.historical_data[0] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-02T23:40:00Z', 'endTimeUnix': 1709422800, 'energyMode': 'peak', 'startTime': '2024-03-02T23:10:01Z', 'startTimeUnix': 1709421000} - assert module.historical_data[-1] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-05T23:10:00Z', 'endTimeUnix': 1709680200, 'energyMode': 'peak', 'startTime': '2024-03-05T22:40:01Z', 'startTimeUnix': 1709678400} + assert module.historical_data[0] == {"Wh": 0, "duration": 30, "endTime": "2024-03-02T23:40:00Z", + "endTimeUnix": 1709422800, "energyMode": "peak", + "startTime": "2024-03-02T23:10:01Z", "startTimeUnix": 1709421000} + assert module.historical_data[-1] == {"Wh": 0, "duration": 30, "endTime": "2024-03-05T23:10:00Z", + "endTimeUnix": 1709680200, "energyMode": "peak", + "startTime": "2024-03-05T22:40:01Z", "startTimeUnix": 1709678400} assert len(module.historical_data) == 134 assert module.sum_energy_elec == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak @@ -82,12 +84,6 @@ async def test_historical_data_retrieval_multi(async_account_multi): assert module.sum_energy_elec_peak == 31282 - - - - - - async def test_historical_data_retrieval_multi_2(async_account_multi): """Test retrieval of historical measurements.""" home_id = "aaaaaaaaaaabbbbbbbbbbccc" @@ -99,9 +95,6 @@ async def test_historical_data_retrieval_multi_2(async_account_multi): module = home.modules[module_id] assert module.device_type == DeviceType.NLC - - - strt = int(dt.datetime.fromisoformat("2024-03-15 00:29:51").timestamp()) end = int(dt.datetime.fromisoformat("2024-03-15 13:45:24").timestamp()) @@ -112,34 +105,37 @@ async def test_historical_data_retrieval_multi_2(async_account_multi): end_time=end ) - - assert module.historical_data[0] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-14T23:59:51Z', 'endTimeUnix': 1710460791, 'energyMode': 'peak', 'startTime': '2024-03-14T23:29:52Z', 'startTimeUnix': 1710458991} - assert module.historical_data[-1] == {'Wh': 0, 'duration': 30, 'endTime': '2024-03-15T12:59:51Z', 'endTimeUnix': 1710507591, 'energyMode': 'peak', 'startTime': '2024-03-15T12:29:52Z', 'startTimeUnix': 1710505791} + assert module.historical_data[0] == {"Wh": 0, "duration": 30, "endTime": "2024-03-14T23:59:51Z", + "endTimeUnix": 1710460791, "energyMode": "peak", + "startTime": "2024-03-14T23:29:52Z", "startTimeUnix": 1710458991} + assert module.historical_data[-1] == {"Wh": 0, "duration": 30, "endTime": "2024-03-15T12:59:51Z", + "endTimeUnix": 1710507591, "energyMode": "peak", + "startTime": "2024-03-15T12:29:52Z", "startTimeUnix": 1710505791} assert len(module.historical_data) == 26 assert module.sum_energy_elec == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak assert module.sum_energy_elec_off_peak == 780 assert module.sum_energy_elec_peak == 890 + async def test_disconnected_main_bridge(async_account_multi): """Test retrieval of historical measurements.""" home_id = "aaaaaaaaaaabbbbbbbbbbccc" with open( - "fixtures/home_multi_status_error_disconnected.json", - encoding="utf-8", + "fixtures/home_multi_status_error_disconnected.json", + encoding="utf-8", ) as json_file: home_status_fixture = json.load(json_file) mock_home_status_resp = MockResponse(home_status_fixture, 200) with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_home_status_resp), + "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", + AsyncMock(return_value=mock_home_status_resp), ) as mock_request: try: await async_account_multi.async_update_status(home_id) except ApiHomeReachabilityError: - pass # expected error + pass # expected error else: assert False - From c04947b825208b8da90bd2da7697ef65ceb97974 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 6 May 2024 14:45:05 +0200 Subject: [PATCH 61/97] Ruff.... then black funny game --- src/pyatmo/auth.py | 4 +- src/pyatmo/const.py | 14 ++--- src/pyatmo/modules/base_class.py | 4 +- src/pyatmo/modules/device_types.py | 2 +- src/pyatmo/modules/module.py | 85 ++++++++++++++++-------------- src/pyatmo/modules/netatmo.py | 2 +- src/pyatmo/person.py | 2 +- src/pyatmo/room.py | 2 +- src/pyatmo/schedule.py | 2 +- 9 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 2f83b601..1d91d6c1 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -2,10 +2,10 @@ from __future__ import annotations -from abc import ABC, abstractmethod import asyncio -from json import JSONDecodeError import logging +from abc import ABC, abstractmethod +from json import JSONDecodeError from typing import Any from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index de21662e..bd87c22f 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -141,9 +141,11 @@ class MeasureInterval(Enum): MONTH = "1month" -MEASURE_INTERVAL_TO_SECONDS = {MeasureInterval.HALF_HOUR: 1800, - MeasureInterval.HOUR: 3600, - MeasureInterval.THREE_HOURS: 10800, - MeasureInterval.DAY: 86400, - MeasureInterval.WEEK: 604800, - MeasureInterval.MONTH: 2592000} +MEASURE_INTERVAL_TO_SECONDS = { + MeasureInterval.HALF_HOUR: 1800, + MeasureInterval.HOUR: 3600, + MeasureInterval.THREE_HOURS: 10800, + MeasureInterval.DAY: 86400, + MeasureInterval.WEEK: 604800, + MeasureInterval.MONTH: 2592000, +} diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index b0950ab8..740623bf 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -2,11 +2,11 @@ from __future__ import annotations -from abc import ABC import bisect +import logging +from abc import ABC from collections.abc import Iterable from dataclasses import dataclass -import logging from operator import itemgetter from typing import TYPE_CHECKING, Any diff --git a/src/pyatmo/modules/device_types.py b/src/pyatmo/modules/device_types.py index e5fbe849..7c0f0e12 100644 --- a/src/pyatmo/modules/device_types.py +++ b/src/pyatmo/modules/device_types.py @@ -2,8 +2,8 @@ from __future__ import annotations -from enum import Enum import logging +from enum import Enum LOG = logging.getLogger(__name__) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 2bd82a4c..14336c91 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -3,8 +3,8 @@ from __future__ import annotations import copy -from datetime import datetime, timedelta, timezone import logging +from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError @@ -641,7 +641,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) d_nrj_wh = dt_h * ( - min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w + min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w ) delta_energy += d_nrj_wh @@ -649,7 +649,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): return delta_energy def get_sum_energy_elec_power_adapted( - self, to_ts: int | float | None = None, conservative: bool = False + self, to_ts: int | float | None = None, conservative: bool = False ): """Compute proper energy value with adaptation from power.""" v = self.sum_energy_elec @@ -667,15 +667,17 @@ def get_sum_energy_elec_power_adapted( from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus if ( - from_ts is not None - and from_ts < to_ts - and isinstance(self, PowerMixin) - and isinstance(self, NetatmoBase) + from_ts is not None + and from_ts < to_ts + and isinstance(self, PowerMixin) + and isinstance(self, NetatmoBase) ): power_data = self.get_history_data( "power", from_ts=from_ts, to_ts=to_ts ) - if isinstance(self, EnergyHistoryMixin): #well to please the linter.... + if isinstance( + self, EnergyHistoryMixin + ): # well to please the linter.... delta_energy = self.compute_rieman_sum(power_data, conservative) return v, delta_energy @@ -703,11 +705,11 @@ def update_measures_num_calls(self): return len(self.home.energy_endpoints) async def async_update_measures( - self, - start_time: int | None = None, - end_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, + self, + start_time: int | None = None, + end_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7, ) -> int | None: """Update historical data.""" @@ -791,15 +793,15 @@ async def async_update_measures( return num_calls async def _prepare_exported_historical_data( - self, - start_time, - end_time, - delta_range, - hist_good_vals, - prev_end_time, - prev_start_time, - prev_sum_energy_elec, - peak_off_peak_mode, + self, + start_time, + end_time, + delta_range, + hist_good_vals, + prev_end_time, + prev_start_time, + prev_sum_energy_elec, + peak_off_peak_mode, ): computed_start = 0 computed_end = 0 @@ -840,8 +842,8 @@ async def _prepare_exported_historical_data( }, ) if ( - prev_sum_energy_elec is not None - and prev_sum_energy_elec > self.sum_energy_elec + prev_sum_energy_elec is not None + and prev_sum_energy_elec > self.sum_energy_elec ): msg = ( "ENERGY GOING DOWN %s from: %s to %s " @@ -880,14 +882,14 @@ async def _prepare_exported_historical_data( ) async def _get_aligned_energy_values_and_mode( - self, - start_time, - end_time, - delta_range, - energy_schedule_vals, - peak_off_peak_mode, - raw_datas, - data_points, + self, + start_time, + end_time, + delta_range, + energy_schedule_vals, + peak_off_peak_mode, + raw_datas, + data_points, ): hist_good_vals = [] for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): @@ -941,8 +943,8 @@ async def _get_aligned_energy_values_and_mode( ) if ( - self.home.energy_schedule_vals[idx_limit][1] - != cur_peak_or_off_peak_mode + self.home.energy_schedule_vals[idx_limit][1] + != cur_peak_or_off_peak_mode ): # we are NOT in a proper schedule time for this time span ... @@ -964,8 +966,8 @@ async def _get_aligned_energy_values_and_mode( else: # by construction of the energy schedule the next one should be of opposite mode if ( - energy_schedule_vals[idx_limit + 1][1] - != cur_peak_or_off_peak_mode + energy_schedule_vals[idx_limit + 1][1] + != cur_peak_or_off_peak_mode ): self._log_energy_error( start_time, @@ -981,12 +983,12 @@ async def _get_aligned_energy_values_and_mode( start_time_to_get_closer = energy_schedule_vals[ idx_limit + 1 - ][0] + ][0] diff_t = start_time_to_get_closer - srt_mid cur_start_time = ( - day_origin - + srt_beg - + (diff_t // interval_sec + 1) * interval_sec + day_origin + + srt_beg + + (diff_t // interval_sec + 1) * interval_sec ) hist_good_vals.append( @@ -998,7 +1000,7 @@ async def _get_aligned_energy_values_and_mode( return hist_good_vals async def _compute_proper_energy_schedule_offsets( - self, start_time, end_time, interval_sec, raw_datas, data_points + self, start_time, end_time, interval_sec, raw_datas, data_points ): max_interval_sec = interval_sec for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): @@ -1170,4 +1172,5 @@ class Fan(FirmwareMixin, FanSpeedMixin, PowerMixin, Module): ... + # pylint: enable=too-many-ancestors diff --git a/src/pyatmo/modules/netatmo.py b/src/pyatmo/modules/netatmo.py index 98d4be90..f8bc4441 100644 --- a/src/pyatmo/modules/netatmo.py +++ b/src/pyatmo/modules/netatmo.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass import logging +from dataclasses import dataclass from typing import Any from pyatmo.const import ( diff --git a/src/pyatmo/person.py b/src/pyatmo/person.py index 8e413646..6c7392b8 100644 --- a/src/pyatmo/person.py +++ b/src/pyatmo/person.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass import logging +from dataclasses import dataclass from typing import TYPE_CHECKING from pyatmo.const import RawData diff --git a/src/pyatmo/room.py b/src/pyatmo/room.py index 337049f7..f21fbacb 100644 --- a/src/pyatmo/room.py +++ b/src/pyatmo/room.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass import logging +from dataclasses import dataclass from typing import TYPE_CHECKING, Any from pyatmo.const import FROSTGUARD, HOME, MANUAL, SETROOMTHERMPOINT_ENDPOINT, RawData diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index fdbd8196..ff7b58f7 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass import logging +from dataclasses import dataclass from typing import TYPE_CHECKING from pyatmo.const import ( From 54718f64b9ecd9574add558d19faedf5febb41bd Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 6 May 2024 14:54:41 +0200 Subject: [PATCH 62/97] black.... then ruff funny game --- src/pyatmo/auth.py | 4 +- src/pyatmo/modules/base_class.py | 4 +- src/pyatmo/modules/device_types.py | 2 +- src/pyatmo/modules/module.py | 83 +++++++++++++++--------------- src/pyatmo/modules/netatmo.py | 2 +- src/pyatmo/person.py | 2 +- src/pyatmo/room.py | 2 +- src/pyatmo/schedule.py | 2 +- 8 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 1d91d6c1..2f83b601 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -2,10 +2,10 @@ from __future__ import annotations -import asyncio -import logging from abc import ABC, abstractmethod +import asyncio from json import JSONDecodeError +import logging from typing import Any from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 740623bf..b0950ab8 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -2,11 +2,11 @@ from __future__ import annotations -import bisect -import logging from abc import ABC +import bisect from collections.abc import Iterable from dataclasses import dataclass +import logging from operator import itemgetter from typing import TYPE_CHECKING, Any diff --git a/src/pyatmo/modules/device_types.py b/src/pyatmo/modules/device_types.py index 7c0f0e12..e5fbe849 100644 --- a/src/pyatmo/modules/device_types.py +++ b/src/pyatmo/modules/device_types.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from enum import Enum +import logging LOG = logging.getLogger(__name__) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 14336c91..a2c122ae 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -3,8 +3,8 @@ from __future__ import annotations import copy -import logging from datetime import datetime, timedelta, timezone +import logging from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError @@ -641,7 +641,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) d_nrj_wh = dt_h * ( - min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w + min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w ) delta_energy += d_nrj_wh @@ -649,7 +649,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): return delta_energy def get_sum_energy_elec_power_adapted( - self, to_ts: int | float | None = None, conservative: bool = False + self, to_ts: int | float | None = None, conservative: bool = False ): """Compute proper energy value with adaptation from power.""" v = self.sum_energy_elec @@ -667,16 +667,16 @@ def get_sum_energy_elec_power_adapted( from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus if ( - from_ts is not None - and from_ts < to_ts - and isinstance(self, PowerMixin) - and isinstance(self, NetatmoBase) + from_ts is not None + and from_ts < to_ts + and isinstance(self, PowerMixin) + and isinstance(self, NetatmoBase) ): power_data = self.get_history_data( "power", from_ts=from_ts, to_ts=to_ts ) if isinstance( - self, EnergyHistoryMixin + self, EnergyHistoryMixin ): # well to please the linter.... delta_energy = self.compute_rieman_sum(power_data, conservative) @@ -705,11 +705,11 @@ def update_measures_num_calls(self): return len(self.home.energy_endpoints) async def async_update_measures( - self, - start_time: int | None = None, - end_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, + self, + start_time: int | None = None, + end_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7, ) -> int | None: """Update historical data.""" @@ -793,15 +793,15 @@ async def async_update_measures( return num_calls async def _prepare_exported_historical_data( - self, - start_time, - end_time, - delta_range, - hist_good_vals, - prev_end_time, - prev_start_time, - prev_sum_energy_elec, - peak_off_peak_mode, + self, + start_time, + end_time, + delta_range, + hist_good_vals, + prev_end_time, + prev_start_time, + prev_sum_energy_elec, + peak_off_peak_mode, ): computed_start = 0 computed_end = 0 @@ -842,8 +842,8 @@ async def _prepare_exported_historical_data( }, ) if ( - prev_sum_energy_elec is not None - and prev_sum_energy_elec > self.sum_energy_elec + prev_sum_energy_elec is not None + and prev_sum_energy_elec > self.sum_energy_elec ): msg = ( "ENERGY GOING DOWN %s from: %s to %s " @@ -882,14 +882,14 @@ async def _prepare_exported_historical_data( ) async def _get_aligned_energy_values_and_mode( - self, - start_time, - end_time, - delta_range, - energy_schedule_vals, - peak_off_peak_mode, - raw_datas, - data_points, + self, + start_time, + end_time, + delta_range, + energy_schedule_vals, + peak_off_peak_mode, + raw_datas, + data_points, ): hist_good_vals = [] for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): @@ -943,8 +943,8 @@ async def _get_aligned_energy_values_and_mode( ) if ( - self.home.energy_schedule_vals[idx_limit][1] - != cur_peak_or_off_peak_mode + self.home.energy_schedule_vals[idx_limit][1] + != cur_peak_or_off_peak_mode ): # we are NOT in a proper schedule time for this time span ... @@ -966,8 +966,8 @@ async def _get_aligned_energy_values_and_mode( else: # by construction of the energy schedule the next one should be of opposite mode if ( - energy_schedule_vals[idx_limit + 1][1] - != cur_peak_or_off_peak_mode + energy_schedule_vals[idx_limit + 1][1] + != cur_peak_or_off_peak_mode ): self._log_energy_error( start_time, @@ -983,12 +983,12 @@ async def _get_aligned_energy_values_and_mode( start_time_to_get_closer = energy_schedule_vals[ idx_limit + 1 - ][0] + ][0] diff_t = start_time_to_get_closer - srt_mid cur_start_time = ( - day_origin - + srt_beg - + (diff_t // interval_sec + 1) * interval_sec + day_origin + + srt_beg + + (diff_t // interval_sec + 1) * interval_sec ) hist_good_vals.append( @@ -1000,7 +1000,7 @@ async def _get_aligned_energy_values_and_mode( return hist_good_vals async def _compute_proper_energy_schedule_offsets( - self, start_time, end_time, interval_sec, raw_datas, data_points + self, start_time, end_time, interval_sec, raw_datas, data_points ): max_interval_sec = interval_sec for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): @@ -1172,5 +1172,4 @@ class Fan(FirmwareMixin, FanSpeedMixin, PowerMixin, Module): ... - # pylint: enable=too-many-ancestors diff --git a/src/pyatmo/modules/netatmo.py b/src/pyatmo/modules/netatmo.py index f8bc4441..98d4be90 100644 --- a/src/pyatmo/modules/netatmo.py +++ b/src/pyatmo/modules/netatmo.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import Any from pyatmo.const import ( diff --git a/src/pyatmo/person.py b/src/pyatmo/person.py index 6c7392b8..8e413646 100644 --- a/src/pyatmo/person.py +++ b/src/pyatmo/person.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING from pyatmo.const import RawData diff --git a/src/pyatmo/room.py b/src/pyatmo/room.py index f21fbacb..337049f7 100644 --- a/src/pyatmo/room.py +++ b/src/pyatmo/room.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING, Any from pyatmo.const import FROSTGUARD, HOME, MANUAL, SETROOMTHERMPOINT_ENDPOINT, RawData diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index ff7b58f7..fdbd8196 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -2,8 +2,8 @@ from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING from pyatmo.const import ( From 52bf849f1d9fae1fe07d4d9b52e643d24bf619cb Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 6 May 2024 15:03:53 +0200 Subject: [PATCH 63/97] black and ruff game --- src/pyatmo/account.py | 522 +++++++++++++++++++++-------------------- src/pyatmo/auth.py | 14 +- src/pyatmo/home.py | 22 +- src/pyatmo/schedule.py | 14 +- 4 files changed, 307 insertions(+), 265 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index afe6dd04..74850322 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -34,10 +34,12 @@ class AsyncAccount: """Async class of a Netatmo account.""" - def __init__(self, - auth: AbstractAsyncAuth, - favorite_stations: bool = True, - support_only_homes: list | None = None) -> None: + def __init__( + self, + auth: AbstractAsyncAuth, + favorite_stations: bool = True, + support_only_homes: list | None = None, + ) -> None: """Initialize the Netatmo account.""" self.auth: AbstractAsyncAuth = auth @@ -51,280 +53,298 @@ def __init__(self, self.public_weather_areas: dict[str, modules.PublicWeatherArea] = {} self.modules: dict[str, Module] = {} - def __repr__(self) -> str: - """Return the representation.""" - - return ( - f"{self.__class__.__name__}(user={self.user}, home_ids={self.homes.keys()}" - ) - - def update_supported_homes(self, support_only_homes: list | None = None): - """Update the exposed/supported homes.""" - if support_only_homes is None or len(support_only_homes) == 0: - self.homes = copy.copy(self.all_account_homes) - else: - self.homes = {} - for h_id in support_only_homes: - h = self.all_account_homes.get(h_id) - if h is not None: - self.homes[h_id] = h - - if len(self.homes) == 0: - self.homes = copy.copy(self.all_account_homes) +def __repr__(self) -> str: + """Return the representation.""" - self.support_only_homes = list(self.homes) + return ( + f"{self.__class__.__name__}(user={self.user}, home_ids={self.homes.keys()}" + ) - self.homes.update(self.additional_public_homes) - def process_topology(self) -> None: - """Process topology information from /homesdata.""" +def update_supported_homes(self, support_only_homes: list | None = None): + """Update the exposed/supported homes.""" - for home in self.raw_data["homes"]: - if (home_id := home["id"]) in self.all_account_homes: - self.all_account_homes[home_id].update_topology(home) - else: - self.all_account_homes[home_id] = Home(self.auth, raw_data=home) + if support_only_homes is None or len(support_only_homes) == 0: + self.homes = copy.copy(self.all_account_homes) + else: + self.homes = {} + for h_id in support_only_homes: + h = self.all_account_homes.get(h_id) + if h is not None: + self.homes[h_id] = h - self.update_supported_homes(self.support_only_homes) + if len(self.homes) == 0: + self.homes = copy.copy(self.all_account_homes) - async def async_update_topology(self) -> int: - """Retrieve topology data from /homesdata. Returns the number of performed API calls.""" + self.support_only_homes = list(self.homes) - resp = await self.auth.async_post_api_request( - endpoint=GETHOMESDATA_ENDPOINT, - ) - self.raw_data = extract_raw_data(await resp.json(), "homes") + self.homes.update(self.additional_public_homes) - self.user = self.raw_data.get("user", {}).get("email") - self.process_topology() +def process_topology(self) -> None: + """Process topology information from /homesdata.""" - return 1 - - async def async_update_status(self, home_id: str | None = None) -> int: - """Retrieve status data from /homestatus. Returns the number of performed API calls.""" - - if home_id is None: - self.update_supported_homes(self.support_only_homes) - homes = self.homes + for home in self.raw_data["homes"]: + if (home_id := home["id"]) in self.all_account_homes: + self.all_account_homes[home_id].update_topology(home) else: - homes = [home_id] - num_calls = 0 - all_homes_ok = True - for h_id in homes: - resp = await self.auth.async_post_api_request( - endpoint=GETHOMESTATUS_ENDPOINT, - params={"home_id": h_id}, - ) - raw_data = extract_raw_data(await resp.json(), HOME) - is_correct_update = await self.homes[h_id].update(raw_data) - if not is_correct_update: - all_homes_ok = False - num_calls += 1 + self.all_account_homes[home_id] = Home(self.auth, raw_data=home) - if all_homes_ok is False: - raise ApiHomeReachabilityError( - "No Home update could be performed, all modules unreachable and not updated", ) + self.update_supported_homes(self.support_only_homes) - return num_calls - async def async_update_events(self, home_id: str) -> int: - """Retrieve events from /getevents. Returns the number of performed API calls.""" - resp = await self.auth.async_post_api_request( - endpoint=GETEVENTS_ENDPOINT, - params={"home_id": home_id}, - ) - raw_data = extract_raw_data(await resp.json(), HOME) - await self.homes[home_id].update(raw_data) +async def async_update_topology(self) -> int: + """Retrieve topology data from /homesdata. Returns the number of performed API calls.""" - return 1 + resp = await self.auth.async_post_api_request( + endpoint=GETHOMESDATA_ENDPOINT, + ) + self.raw_data = extract_raw_data(await resp.json(), "homes") - async def async_update_weather_stations(self) -> int: - """Retrieve status data from /getstationsdata. Returns the number of performed API calls.""" - params = {"get_favorites": ("true" if self.favorite_stations else "false")} - await self._async_update_data( - GETSTATIONDATA_ENDPOINT, - params=params, - ) - return 1 + self.user = self.raw_data.get("user", {}).get("email") - async def async_update_air_care(self) -> int: - """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls.""" - await self._async_update_data(GETHOMECOACHDATA_ENDPOINT) + self.process_topology() - return 1 + return 1 - async def async_update_measures( - self, - home_id: str, - module_id: str, - start_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, - end_time: int | None = None - ) -> int: - """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" - - num_calls = await getattr(self.homes[home_id].modules[module_id], "async_update_measures")( - start_time=start_time, - end_time=end_time, - interval=interval, - days=days, - ) - return num_calls - def register_public_weather_area( - self, - lat_ne: str, - lon_ne: str, - lat_sw: str, - lon_sw: str, - required_data_type: str | None = None, - filtering: bool = False, - *, - area_id: str = str(uuid4()), - ) -> str: - """Register public weather area to monitor.""" - - self.public_weather_areas[area_id] = modules.PublicWeatherArea( - lat_ne, - lon_ne, - lat_sw, - lon_sw, - required_data_type, - filtering, - ) - return area_id - - async def async_update_public_weather(self, area_id: str) -> int: - """Retrieve status data from /getpublicdata. Returns the number of performed API calls.""" - params = { - "lat_ne": self.public_weather_areas[area_id].location.lat_ne, - "lon_ne": self.public_weather_areas[area_id].location.lon_ne, - "lat_sw": self.public_weather_areas[area_id].location.lat_sw, - "lon_sw": self.public_weather_areas[area_id].location.lon_sw, - "filtering": ( - "true" if self.public_weather_areas[area_id].filtering else "false" - ), - } - await self._async_update_data( - GETPUBLIC_DATA_ENDPOINT, - tag="body", - params=params, - area_id=area_id, - ) +async def async_update_status(self, home_id: str | None = None) -> int: + """Retrieve status data from /homestatus. Returns the number of performed API calls.""" - return 1 - - async def _async_update_data( - self, - endpoint: str, - params: dict[str, Any] | None = None, - tag: str = "devices", - area_id: str | None = None, - ) -> None: - """Retrieve status data from .""" - resp = await self.auth.async_post_api_request(endpoint=endpoint, params=params) - raw_data = extract_raw_data(await resp.json(), tag) - await self.update_devices(raw_data, area_id) - - async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: - """Modify device state by passing JSON specific to the device. Returns the number of performed API calls.""" - LOG.debug("Setting state: %s", data) - - post_params = { - "json": { - HOME: { - "id": home_id, - **data, - }, - }, - } + if home_id is None: + self.update_supported_homes(self.support_only_homes) + homes = self.homes + else: + homes = [home_id] + num_calls = 0 + all_homes_ok = True + for h_id in homes: resp = await self.auth.async_post_api_request( - endpoint=SETSTATE_ENDPOINT, - params=post_params, + endpoint=GETHOMESTATUS_ENDPOINT, + params={"home_id": h_id}, + ) + raw_data = extract_raw_data(await resp.json(), HOME) + is_correct_update = await self.homes[h_id].update(raw_data) + if not is_correct_update: + all_homes_ok = False + num_calls += 1 + + if all_homes_ok is False: + raise ApiHomeReachabilityError( + "No Home update could be performed, all modules unreachable and not updated", ) - LOG.debug("Response: %s", resp) - return 1 - async def update_devices( - self, - raw_data: RawData, - area_id: str | None = None, - ) -> None: - """Update device states.""" - for device_data in raw_data.get("devices", {}): - if home_id := device_data.get( - "home_id", - self.find_home_of_device(device_data), - ): - if home_id not in self.homes: - modules_data = [] - for module_data in device_data.get("modules", []): - module_data["home_id"] = home_id - module_data["id"] = module_data["_id"] - module_data["name"] = module_data.get("module_name") - modules_data.append(normalize_weather_attributes(module_data)) - modules_data.append(normalize_weather_attributes(device_data)) - - self.additional_public_homes[home_id] = Home( - self.auth, - raw_data={ - "id": home_id, - "name": device_data.get("home_name", "Unknown"), - "modules": modules_data, - }, - ) - self.update_supported_homes(self.support_only_homes) - - await self.homes[home_id].update( - {HOME: {"modules": [normalize_weather_attributes(device_data)]}}, + return num_calls + + +async def async_update_events(self, home_id: str) -> int: + """Retrieve events from /getevents. Returns the number of performed API calls.""" + resp = await self.auth.async_post_api_request( + endpoint=GETEVENTS_ENDPOINT, + params={"home_id": home_id}, + ) + raw_data = extract_raw_data(await resp.json(), HOME) + await self.homes[home_id].update(raw_data) + + return 1 + + +async def async_update_weather_stations(self) -> int: + """Retrieve status data from /getstationsdata. Returns the number of performed API calls.""" + params = {"get_favorites": ("true" if self.favorite_stations else "false")} + await self._async_update_data( + GETSTATIONDATA_ENDPOINT, + params=params, + ) + return 1 + + +async def async_update_air_care(self) -> int: + """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls.""" + await self._async_update_data(GETHOMECOACHDATA_ENDPOINT) + + return 1 + + +async def async_update_measures( + self, + home_id: str, + module_id: str, + start_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7, + end_time: int | None = None, +) -> int: + """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" + + num_calls = await getattr( + self.homes[home_id].modules[module_id], "async_update_measures" + )( + start_time=start_time, + end_time=end_time, + interval=interval, + days=days, + ) + return num_calls + + +def register_public_weather_area( + self, + lat_ne: str, + lon_ne: str, + lat_sw: str, + lon_sw: str, + required_data_type: str | None = None, + filtering: bool = False, + *, + area_id: str = str(uuid4()), +) -> str: + """Register public weather area to monitor.""" + + self.public_weather_areas[area_id] = modules.PublicWeatherArea( + lat_ne, + lon_ne, + lat_sw, + lon_sw, + required_data_type, + filtering, + ) + return area_id + + +async def async_update_public_weather(self, area_id: str) -> int: + """Retrieve status data from /getpublicdata. Returns the number of performed API calls.""" + params = { + "lat_ne": self.public_weather_areas[area_id].location.lat_ne, + "lon_ne": self.public_weather_areas[area_id].location.lon_ne, + "lat_sw": self.public_weather_areas[area_id].location.lat_sw, + "lon_sw": self.public_weather_areas[area_id].location.lon_sw, + "filtering": ( + "true" if self.public_weather_areas[area_id].filtering else "false" + ), + } + await self._async_update_data( + GETPUBLIC_DATA_ENDPOINT, + tag="body", + params=params, + area_id=area_id, + ) + + return 1 + + +async def _async_update_data( + self, + endpoint: str, + params: dict[str, Any] | None = None, + tag: str = "devices", + area_id: str | None = None, +) -> None: + """Retrieve status data from .""" + resp = await self.auth.async_post_api_request(endpoint=endpoint, params=params) + raw_data = extract_raw_data(await resp.json(), tag) + await self.update_devices(raw_data, area_id) + + +async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: + """Modify device state by passing JSON specific to the device. Returns the number of performed API calls.""" + LOG.debug("Setting state: %s", data) + + post_params = { + "json": { + HOME: { + "id": home_id, + **data, + }, + }, + } + resp = await self.auth.async_post_api_request( + endpoint=SETSTATE_ENDPOINT, + params=post_params, + ) + LOG.debug("Response: %s", resp) + return 1 + + +async def update_devices( + self, + raw_data: RawData, + area_id: str | None = None, +) -> None: + """Update device states.""" + for device_data in raw_data.get("devices", {}): + if home_id := device_data.get( + "home_id", + self.find_home_of_device(device_data), + ): + if home_id not in self.homes: + modules_data = [] + for module_data in device_data.get("modules", []): + module_data["home_id"] = home_id + module_data["id"] = module_data["_id"] + module_data["name"] = module_data.get("module_name") + modules_data.append(normalize_weather_attributes(module_data)) + modules_data.append(normalize_weather_attributes(device_data)) + + self.additional_public_homes[home_id] = Home( + self.auth, + raw_data={ + "id": home_id, + "name": device_data.get("home_name", "Unknown"), + "modules": modules_data, + }, ) - else: - LOG.debug("No home %s found.", home_id) - - for module_data in device_data.get("modules", []): - module_data["home_id"] = home_id - await self.update_devices({"devices": [module_data]}) - - if ( - device_data["type"] == "NHC" - or self.find_home_of_device(device_data) is None - ): - device_data["name"] = device_data.get( - "station_name", - device_data.get("module_name", "Unknown"), + self.update_supported_homes(self.support_only_homes) + + await self.homes[home_id].update( + {HOME: {"modules": [normalize_weather_attributes(device_data)]}}, + ) + else: + LOG.debug("No home %s found.", home_id) + + for module_data in device_data.get("modules", []): + module_data["home_id"] = home_id + await self.update_devices({"devices": [module_data]}) + + if ( + device_data["type"] == "NHC" + or self.find_home_of_device(device_data) is None + ): + device_data["name"] = device_data.get( + "station_name", + device_data.get("module_name", "Unknown"), + ) + device_data = normalize_weather_attributes(device_data) + if device_data["id"] not in self.modules: + self.modules[device_data["id"]] = getattr( + modules, + device_data["type"], + )( + home=self, + module=device_data, ) - device_data = normalize_weather_attributes(device_data) - if device_data["id"] not in self.modules: - self.modules[device_data["id"]] = getattr( - modules, - device_data["type"], - )( - home=self, - module=device_data, - ) - await self.modules[device_data["id"]].update(device_data) - - if device_data.get("modules", []): - self.modules[device_data["id"]].modules = [ - module["_id"] for module in device_data["modules"] - ] - - if area_id is not None: - self.public_weather_areas[area_id].update(raw_data) - - def find_home_of_device(self, device_data: dict[str, Any]) -> str | None: - """Find home_id of device.""" - return next( - ( - home_id - for home_id, home in self.homes.items() - if device_data["_id"] in home.modules - ), - None, - ) + await self.modules[device_data["id"]].update(device_data) + + if device_data.get("modules", []): + self.modules[device_data["id"]].modules = [ + module["_id"] for module in device_data["modules"] + ] + + if area_id is not None: + self.public_weather_areas[area_id].update(raw_data) + + +def find_home_of_device(self, device_data: dict[str, Any]) -> str | None: + """Find home_id of device.""" + return next( + ( + home_id + for home_id, home in self.homes.items() + if device_data["_id"] in home.modules + ), + None, + ) ATTRIBUTES_TO_FIX = { diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 2f83b601..28e7fae2 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -187,13 +187,19 @@ async def handle_error_response(self, resp, resp_status, url): try: resp_json = await resp.json() - message = (f"{resp_status} - {ERRORS.get(resp_status, '')} - {resp_json['error']['message']} " - f"({resp_json['error']['code']}) when accessing '{url}'") + message = ( + f"{resp_status} - {ERRORS.get(resp_status, '')} - {resp_json['error']['message']} " + f"({resp_json['error']['code']}) when accessing '{url}'" + ) if resp_status == 403 and resp_json["error"]["code"] == 26: - raise ApiErrorThrottling(message, ) + raise ApiErrorThrottling( + message, + ) else: - raise ApiError(message, ) + raise ApiError( + message, + ) except (JSONDecodeError, ContentTypeError) as exc: raise ApiError( diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 55eeced9..826a5168 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -92,7 +92,9 @@ def _handle_schedules(self, raw_data): self.all_schedules = schedules - nrj_schedule = next(iter(schedules.get(SCHEDULE_TYPE_ELECTRICITY, {}).values()), None) + nrj_schedule = next( + iter(schedules.get(SCHEDULE_TYPE_ELECTRICITY, {}).values()), None + ) self.energy_schedule_vals = [] self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] @@ -106,8 +108,12 @@ def _handle_schedules(self, raw_data): self.energy_endpoints = [None, None] - self.energy_endpoints[ENERGY_ELEC_PEAK_IDX] = MeasureType.SUM_ENERGY_ELEC_PEAK.value - self.energy_endpoints[ENERGY_ELEC_OFF_IDX] = MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value + self.energy_endpoints[ENERGY_ELEC_PEAK_IDX] = ( + MeasureType.SUM_ENERGY_ELEC_PEAK.value + ) + self.energy_endpoints[ENERGY_ELEC_OFF_IDX] = ( + MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value + ) if zones[0].price_type == "peak": peak_id = zones[0].entity_id @@ -119,7 +125,9 @@ def _handle_schedules(self, raw_data): # timetable are daily for electricity type, and sorted from begining to end for t in timetable: - time = t.m_offset * 60 # m_offset is in minute from the begininng of the day + time = ( + t.m_offset * 60 + ) # m_offset is in minute from the begininng of the day if len(self.energy_schedule_vals) == 0: time = 0 @@ -225,7 +233,11 @@ async def update(self, raw_data: RawData) -> bool: ], ) - if num_errors > 0 and has_one_module_reachable is False and has_an_update is False: + if ( + num_errors > 0 + and has_one_module_reachable is False + and has_an_update is False + ): return False return True diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index fdbd8196..98f413fe 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -81,7 +81,9 @@ class CoolingSchedule(ThermSchedule): def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize CoolingSchedule.""" super().__init__(home, raw_data) - self.cooling_away_temp = self.away_temp = raw_data.get("cooling_away_temp", self.away_temp) + self.cooling_away_temp = self.away_temp = raw_data.get( + "cooling_away_temp", self.away_temp + ) @dataclass @@ -214,8 +216,10 @@ def schedule_factory(home: Home, raw_data: RawData) -> (Schedule, str): """Create proper schedules.""" schedule_type = raw_data.get("type", "custom") - cls = {SCHEDULE_TYPE_THERM: ThermSchedule, - SCHEDULE_TYPE_EVENT: EventSchedule, - SCHEDULE_TYPE_ELECTRICITY: ElectricitySchedule, - SCHEDULE_TYPE_COOLING: CoolingSchedule}.get(schedule_type, Schedule) + cls = { + SCHEDULE_TYPE_THERM: ThermSchedule, + SCHEDULE_TYPE_EVENT: EventSchedule, + SCHEDULE_TYPE_ELECTRICITY: ElectricitySchedule, + SCHEDULE_TYPE_COOLING: CoolingSchedule, + }.get(schedule_type, Schedule) return cls(home, raw_data), schedule_type From 72796c64d84756d95a6507db5fa827a1dc786339 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 6 May 2024 15:09:42 +0200 Subject: [PATCH 64/97] black and ruff game --- src/pyatmo/account.py | 526 +++++++++++++++++++++--------------------- 1 file changed, 263 insertions(+), 263 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 74850322..4e88eb7b 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -54,297 +54,297 @@ def __init__( self.modules: dict[str, Module] = {} -def __repr__(self) -> str: - """Return the representation.""" + def __repr__(self) -> str: + """Return the representation.""" - return ( - f"{self.__class__.__name__}(user={self.user}, home_ids={self.homes.keys()}" - ) + return ( + f"{self.__class__.__name__}(user={self.user}, home_ids={self.homes.keys()}" + ) -def update_supported_homes(self, support_only_homes: list | None = None): - """Update the exposed/supported homes.""" + def update_supported_homes(self, support_only_homes: list | None = None): + """Update the exposed/supported homes.""" - if support_only_homes is None or len(support_only_homes) == 0: - self.homes = copy.copy(self.all_account_homes) - else: - self.homes = {} - for h_id in support_only_homes: - h = self.all_account_homes.get(h_id) - if h is not None: - self.homes[h_id] = h + if support_only_homes is None or len(support_only_homes) == 0: + self.homes = copy.copy(self.all_account_homes) + else: + self.homes = {} + for h_id in support_only_homes: + h = self.all_account_homes.get(h_id) + if h is not None: + self.homes[h_id] = h - if len(self.homes) == 0: - self.homes = copy.copy(self.all_account_homes) + if len(self.homes) == 0: + self.homes = copy.copy(self.all_account_homes) - self.support_only_homes = list(self.homes) + self.support_only_homes = list(self.homes) - self.homes.update(self.additional_public_homes) + self.homes.update(self.additional_public_homes) -def process_topology(self) -> None: - """Process topology information from /homesdata.""" + def process_topology(self) -> None: + """Process topology information from /homesdata.""" - for home in self.raw_data["homes"]: - if (home_id := home["id"]) in self.all_account_homes: - self.all_account_homes[home_id].update_topology(home) - else: - self.all_account_homes[home_id] = Home(self.auth, raw_data=home) + for home in self.raw_data["homes"]: + if (home_id := home["id"]) in self.all_account_homes: + self.all_account_homes[home_id].update_topology(home) + else: + self.all_account_homes[home_id] = Home(self.auth, raw_data=home) - self.update_supported_homes(self.support_only_homes) + self.update_supported_homes(self.support_only_homes) -async def async_update_topology(self) -> int: - """Retrieve topology data from /homesdata. Returns the number of performed API calls.""" + async def async_update_topology(self) -> int: + """Retrieve topology data from /homesdata. Returns the number of performed API calls.""" - resp = await self.auth.async_post_api_request( - endpoint=GETHOMESDATA_ENDPOINT, - ) - self.raw_data = extract_raw_data(await resp.json(), "homes") + resp = await self.auth.async_post_api_request( + endpoint=GETHOMESDATA_ENDPOINT, + ) + self.raw_data = extract_raw_data(await resp.json(), "homes") - self.user = self.raw_data.get("user", {}).get("email") + self.user = self.raw_data.get("user", {}).get("email") - self.process_topology() + self.process_topology() - return 1 + return 1 -async def async_update_status(self, home_id: str | None = None) -> int: - """Retrieve status data from /homestatus. Returns the number of performed API calls.""" + async def async_update_status(self, home_id: str | None = None) -> int: + """Retrieve status data from /homestatus. Returns the number of performed API calls.""" - if home_id is None: - self.update_supported_homes(self.support_only_homes) - homes = self.homes - else: - homes = [home_id] - num_calls = 0 - all_homes_ok = True - for h_id in homes: + if home_id is None: + self.update_supported_homes(self.support_only_homes) + homes = self.homes + else: + homes = [home_id] + num_calls = 0 + all_homes_ok = True + for h_id in homes: + resp = await self.auth.async_post_api_request( + endpoint=GETHOMESTATUS_ENDPOINT, + params={"home_id": h_id}, + ) + raw_data = extract_raw_data(await resp.json(), HOME) + is_correct_update = await self.homes[h_id].update(raw_data) + if not is_correct_update: + all_homes_ok = False + num_calls += 1 + + if all_homes_ok is False: + raise ApiHomeReachabilityError( + "No Home update could be performed, all modules unreachable and not updated", + ) + + return num_calls + + + async def async_update_events(self, home_id: str) -> int: + """Retrieve events from /getevents. Returns the number of performed API calls.""" resp = await self.auth.async_post_api_request( - endpoint=GETHOMESTATUS_ENDPOINT, - params={"home_id": h_id}, + endpoint=GETEVENTS_ENDPOINT, + params={"home_id": home_id}, ) raw_data = extract_raw_data(await resp.json(), HOME) - is_correct_update = await self.homes[h_id].update(raw_data) - if not is_correct_update: - all_homes_ok = False - num_calls += 1 - - if all_homes_ok is False: - raise ApiHomeReachabilityError( - "No Home update could be performed, all modules unreachable and not updated", + await self.homes[home_id].update(raw_data) + + return 1 + + + async def async_update_weather_stations(self) -> int: + """Retrieve status data from /getstationsdata. Returns the number of performed API calls.""" + params = {"get_favorites": ("true" if self.favorite_stations else "false")} + await self._async_update_data( + GETSTATIONDATA_ENDPOINT, + params=params, ) + return 1 + - return num_calls - - -async def async_update_events(self, home_id: str) -> int: - """Retrieve events from /getevents. Returns the number of performed API calls.""" - resp = await self.auth.async_post_api_request( - endpoint=GETEVENTS_ENDPOINT, - params={"home_id": home_id}, - ) - raw_data = extract_raw_data(await resp.json(), HOME) - await self.homes[home_id].update(raw_data) - - return 1 - - -async def async_update_weather_stations(self) -> int: - """Retrieve status data from /getstationsdata. Returns the number of performed API calls.""" - params = {"get_favorites": ("true" if self.favorite_stations else "false")} - await self._async_update_data( - GETSTATIONDATA_ENDPOINT, - params=params, - ) - return 1 - - -async def async_update_air_care(self) -> int: - """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls.""" - await self._async_update_data(GETHOMECOACHDATA_ENDPOINT) - - return 1 - - -async def async_update_measures( - self, - home_id: str, - module_id: str, - start_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, - end_time: int | None = None, -) -> int: - """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" - - num_calls = await getattr( - self.homes[home_id].modules[module_id], "async_update_measures" - )( - start_time=start_time, - end_time=end_time, - interval=interval, - days=days, - ) - return num_calls - - -def register_public_weather_area( - self, - lat_ne: str, - lon_ne: str, - lat_sw: str, - lon_sw: str, - required_data_type: str | None = None, - filtering: bool = False, - *, - area_id: str = str(uuid4()), -) -> str: - """Register public weather area to monitor.""" - - self.public_weather_areas[area_id] = modules.PublicWeatherArea( - lat_ne, - lon_ne, - lat_sw, - lon_sw, - required_data_type, - filtering, - ) - return area_id - - -async def async_update_public_weather(self, area_id: str) -> int: - """Retrieve status data from /getpublicdata. Returns the number of performed API calls.""" - params = { - "lat_ne": self.public_weather_areas[area_id].location.lat_ne, - "lon_ne": self.public_weather_areas[area_id].location.lon_ne, - "lat_sw": self.public_weather_areas[area_id].location.lat_sw, - "lon_sw": self.public_weather_areas[area_id].location.lon_sw, - "filtering": ( - "true" if self.public_weather_areas[area_id].filtering else "false" - ), - } - await self._async_update_data( - GETPUBLIC_DATA_ENDPOINT, - tag="body", - params=params, - area_id=area_id, - ) - - return 1 - - -async def _async_update_data( - self, - endpoint: str, - params: dict[str, Any] | None = None, - tag: str = "devices", - area_id: str | None = None, -) -> None: - """Retrieve status data from .""" - resp = await self.auth.async_post_api_request(endpoint=endpoint, params=params) - raw_data = extract_raw_data(await resp.json(), tag) - await self.update_devices(raw_data, area_id) - - -async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: - """Modify device state by passing JSON specific to the device. Returns the number of performed API calls.""" - LOG.debug("Setting state: %s", data) - - post_params = { - "json": { - HOME: { - "id": home_id, - **data, + async def async_update_air_care(self) -> int: + """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls.""" + await self._async_update_data(GETHOMECOACHDATA_ENDPOINT) + + return 1 + + + async def async_update_measures( + self, + home_id: str, + module_id: str, + start_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7, + end_time: int | None = None, + ) -> int: + """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" + + num_calls = await getattr( + self.homes[home_id].modules[module_id], "async_update_measures" + )( + start_time=start_time, + end_time=end_time, + interval=interval, + days=days, + ) + return num_calls + + + def register_public_weather_area( + self, + lat_ne: str, + lon_ne: str, + lat_sw: str, + lon_sw: str, + required_data_type: str | None = None, + filtering: bool = False, + *, + area_id: str = str(uuid4()), + ) -> str: + """Register public weather area to monitor.""" + + self.public_weather_areas[area_id] = modules.PublicWeatherArea( + lat_ne, + lon_ne, + lat_sw, + lon_sw, + required_data_type, + filtering, + ) + return area_id + + + async def async_update_public_weather(self, area_id: str) -> int: + """Retrieve status data from /getpublicdata. Returns the number of performed API calls.""" + params = { + "lat_ne": self.public_weather_areas[area_id].location.lat_ne, + "lon_ne": self.public_weather_areas[area_id].location.lon_ne, + "lat_sw": self.public_weather_areas[area_id].location.lat_sw, + "lon_sw": self.public_weather_areas[area_id].location.lon_sw, + "filtering": ( + "true" if self.public_weather_areas[area_id].filtering else "false" + ), + } + await self._async_update_data( + GETPUBLIC_DATA_ENDPOINT, + tag="body", + params=params, + area_id=area_id, + ) + + return 1 + + + async def _async_update_data( + self, + endpoint: str, + params: dict[str, Any] | None = None, + tag: str = "devices", + area_id: str | None = None, + ) -> None: + """Retrieve status data from .""" + resp = await self.auth.async_post_api_request(endpoint=endpoint, params=params) + raw_data = extract_raw_data(await resp.json(), tag) + await self.update_devices(raw_data, area_id) + + + async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: + """Modify device state by passing JSON specific to the device. Returns the number of performed API calls.""" + LOG.debug("Setting state: %s", data) + + post_params = { + "json": { + HOME: { + "id": home_id, + **data, + }, }, - }, - } - resp = await self.auth.async_post_api_request( - endpoint=SETSTATE_ENDPOINT, - params=post_params, - ) - LOG.debug("Response: %s", resp) - return 1 - - -async def update_devices( - self, - raw_data: RawData, - area_id: str | None = None, -) -> None: - """Update device states.""" - for device_data in raw_data.get("devices", {}): - if home_id := device_data.get( - "home_id", - self.find_home_of_device(device_data), - ): - if home_id not in self.homes: - modules_data = [] - for module_data in device_data.get("modules", []): - module_data["home_id"] = home_id - module_data["id"] = module_data["_id"] - module_data["name"] = module_data.get("module_name") - modules_data.append(normalize_weather_attributes(module_data)) - modules_data.append(normalize_weather_attributes(device_data)) - - self.additional_public_homes[home_id] = Home( - self.auth, - raw_data={ - "id": home_id, - "name": device_data.get("home_name", "Unknown"), - "modules": modules_data, - }, - ) - self.update_supported_homes(self.support_only_homes) + } + resp = await self.auth.async_post_api_request( + endpoint=SETSTATE_ENDPOINT, + params=post_params, + ) + LOG.debug("Response: %s", resp) + return 1 - await self.homes[home_id].update( - {HOME: {"modules": [normalize_weather_attributes(device_data)]}}, - ) - else: - LOG.debug("No home %s found.", home_id) - - for module_data in device_data.get("modules", []): - module_data["home_id"] = home_id - await self.update_devices({"devices": [module_data]}) - - if ( - device_data["type"] == "NHC" - or self.find_home_of_device(device_data) is None - ): - device_data["name"] = device_data.get( - "station_name", - device_data.get("module_name", "Unknown"), - ) - device_data = normalize_weather_attributes(device_data) - if device_data["id"] not in self.modules: - self.modules[device_data["id"]] = getattr( - modules, - device_data["type"], - )( - home=self, - module=device_data, + + async def update_devices( + self, + raw_data: RawData, + area_id: str | None = None, + ) -> None: + """Update device states.""" + for device_data in raw_data.get("devices", {}): + if home_id := device_data.get( + "home_id", + self.find_home_of_device(device_data), + ): + if home_id not in self.homes: + modules_data = [] + for module_data in device_data.get("modules", []): + module_data["home_id"] = home_id + module_data["id"] = module_data["_id"] + module_data["name"] = module_data.get("module_name") + modules_data.append(normalize_weather_attributes(module_data)) + modules_data.append(normalize_weather_attributes(device_data)) + + self.additional_public_homes[home_id] = Home( + self.auth, + raw_data={ + "id": home_id, + "name": device_data.get("home_name", "Unknown"), + "modules": modules_data, + }, + ) + self.update_supported_homes(self.support_only_homes) + + await self.homes[home_id].update( + {HOME: {"modules": [normalize_weather_attributes(device_data)]}}, ) - await self.modules[device_data["id"]].update(device_data) - - if device_data.get("modules", []): - self.modules[device_data["id"]].modules = [ - module["_id"] for module in device_data["modules"] - ] - - if area_id is not None: - self.public_weather_areas[area_id].update(raw_data) - - -def find_home_of_device(self, device_data: dict[str, Any]) -> str | None: - """Find home_id of device.""" - return next( - ( - home_id - for home_id, home in self.homes.items() - if device_data["_id"] in home.modules - ), - None, - ) + else: + LOG.debug("No home %s found.", home_id) + + for module_data in device_data.get("modules", []): + module_data["home_id"] = home_id + await self.update_devices({"devices": [module_data]}) + + if ( + device_data["type"] == "NHC" + or self.find_home_of_device(device_data) is None + ): + device_data["name"] = device_data.get( + "station_name", + device_data.get("module_name", "Unknown"), + ) + device_data = normalize_weather_attributes(device_data) + if device_data["id"] not in self.modules: + self.modules[device_data["id"]] = getattr( + modules, + device_data["type"], + )( + home=self, + module=device_data, + ) + await self.modules[device_data["id"]].update(device_data) + + if device_data.get("modules", []): + self.modules[device_data["id"]].modules = [ + module["_id"] for module in device_data["modules"] + ] + + if area_id is not None: + self.public_weather_areas[area_id].update(raw_data) + + + def find_home_of_device(self, device_data: dict[str, Any]) -> str | None: + """Find home_id of device.""" + return next( + ( + home_id + for home_id, home in self.homes.items() + if device_data["_id"] in home.modules + ), + None, + ) ATTRIBUTES_TO_FIX = { From 051005fc53ea4741824c24851480622050adc893 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Mon, 6 May 2024 15:16:39 +0200 Subject: [PATCH 65/97] YABT .. yet another Black Test --- src/pyatmo/account.py | 79 +++++++++------------ src/pyatmo/auth.py | 76 ++++++++++---------- src/pyatmo/home.py | 36 +++++----- src/pyatmo/modules/base_class.py | 10 +-- src/pyatmo/modules/module.py | 81 ++++++++++----------- src/pyatmo/modules/netatmo.py | 22 +++--- src/pyatmo/room.py | 40 +++++------ tests/common.py | 2 +- tests/conftest.py | 5 +- tests/test_energy.py | 118 +++++++++++++++++++++---------- tests/test_home.py | 1 - tests/testing_main_template.py | 25 +++---- 12 files changed, 261 insertions(+), 234 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 4e88eb7b..7a6b0bc8 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -35,10 +35,10 @@ class AsyncAccount: """Async class of a Netatmo account.""" def __init__( - self, - auth: AbstractAsyncAuth, - favorite_stations: bool = True, - support_only_homes: list | None = None, + self, + auth: AbstractAsyncAuth, + favorite_stations: bool = True, + support_only_homes: list | None = None, ) -> None: """Initialize the Netatmo account.""" @@ -53,7 +53,6 @@ def __init__( self.public_weather_areas: dict[str, modules.PublicWeatherArea] = {} self.modules: dict[str, Module] = {} - def __repr__(self) -> str: """Return the representation.""" @@ -61,7 +60,6 @@ def __repr__(self) -> str: f"{self.__class__.__name__}(user={self.user}, home_ids={self.homes.keys()}" ) - def update_supported_homes(self, support_only_homes: list | None = None): """Update the exposed/supported homes.""" @@ -81,7 +79,6 @@ def update_supported_homes(self, support_only_homes: list | None = None): self.homes.update(self.additional_public_homes) - def process_topology(self) -> None: """Process topology information from /homesdata.""" @@ -93,7 +90,6 @@ def process_topology(self) -> None: self.update_supported_homes(self.support_only_homes) - async def async_update_topology(self) -> int: """Retrieve topology data from /homesdata. Returns the number of performed API calls.""" @@ -108,7 +104,6 @@ async def async_update_topology(self) -> int: return 1 - async def async_update_status(self, home_id: str | None = None) -> int: """Retrieve status data from /homestatus. Returns the number of performed API calls.""" @@ -137,7 +132,6 @@ async def async_update_status(self, home_id: str | None = None) -> int: return num_calls - async def async_update_events(self, home_id: str) -> int: """Retrieve events from /getevents. Returns the number of performed API calls.""" resp = await self.auth.async_post_api_request( @@ -149,7 +143,6 @@ async def async_update_events(self, home_id: str) -> int: return 1 - async def async_update_weather_stations(self) -> int: """Retrieve status data from /getstationsdata. Returns the number of performed API calls.""" params = {"get_favorites": ("true" if self.favorite_stations else "false")} @@ -159,22 +152,20 @@ async def async_update_weather_stations(self) -> int: ) return 1 - async def async_update_air_care(self) -> int: """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls.""" await self._async_update_data(GETHOMECOACHDATA_ENDPOINT) return 1 - async def async_update_measures( - self, - home_id: str, - module_id: str, - start_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, - end_time: int | None = None, + self, + home_id: str, + module_id: str, + start_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7, + end_time: int | None = None, ) -> int: """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" @@ -188,17 +179,16 @@ async def async_update_measures( ) return num_calls - def register_public_weather_area( - self, - lat_ne: str, - lon_ne: str, - lat_sw: str, - lon_sw: str, - required_data_type: str | None = None, - filtering: bool = False, - *, - area_id: str = str(uuid4()), + self, + lat_ne: str, + lon_ne: str, + lat_sw: str, + lon_sw: str, + required_data_type: str | None = None, + filtering: bool = False, + *, + area_id: str = str(uuid4()), ) -> str: """Register public weather area to monitor.""" @@ -212,7 +202,6 @@ def register_public_weather_area( ) return area_id - async def async_update_public_weather(self, area_id: str) -> int: """Retrieve status data from /getpublicdata. Returns the number of performed API calls.""" params = { @@ -233,20 +222,18 @@ async def async_update_public_weather(self, area_id: str) -> int: return 1 - async def _async_update_data( - self, - endpoint: str, - params: dict[str, Any] | None = None, - tag: str = "devices", - area_id: str | None = None, + self, + endpoint: str, + params: dict[str, Any] | None = None, + tag: str = "devices", + area_id: str | None = None, ) -> None: """Retrieve status data from .""" resp = await self.auth.async_post_api_request(endpoint=endpoint, params=params) raw_data = extract_raw_data(await resp.json(), tag) await self.update_devices(raw_data, area_id) - async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: """Modify device state by passing JSON specific to the device. Returns the number of performed API calls.""" LOG.debug("Setting state: %s", data) @@ -266,17 +253,16 @@ async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: LOG.debug("Response: %s", resp) return 1 - async def update_devices( - self, - raw_data: RawData, - area_id: str | None = None, + self, + raw_data: RawData, + area_id: str | None = None, ) -> None: """Update device states.""" for device_data in raw_data.get("devices", {}): if home_id := device_data.get( - "home_id", - self.find_home_of_device(device_data), + "home_id", + self.find_home_of_device(device_data), ): if home_id not in self.homes: modules_data = [] @@ -308,8 +294,8 @@ async def update_devices( await self.update_devices({"devices": [module_data]}) if ( - device_data["type"] == "NHC" - or self.find_home_of_device(device_data) is None + device_data["type"] == "NHC" + or self.find_home_of_device(device_data) is None ): device_data["name"] = device_data.get( "station_name", @@ -334,7 +320,6 @@ async def update_devices( if area_id is not None: self.public_weather_areas[area_id].update(raw_data) - def find_home_of_device(self, device_data: dict[str, Any]) -> str | None: """Find home_id of device.""" return next( diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 28e7fae2..b4d3ffe8 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -26,9 +26,9 @@ class AbstractAsyncAuth(ABC): """Abstract class to make authenticated requests.""" def __init__( - self, - websession: ClientSession, - base_url: str = DEFAULT_BASE_URL, + self, + websession: ClientSession, + base_url: str = DEFAULT_BASE_URL, ) -> None: """Initialize the auth.""" @@ -40,11 +40,11 @@ async def async_get_access_token(self) -> str: """Return a valid access token.""" async def async_get_image( - self, - endpoint: str, - base_url: str | None = None, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + endpoint: str, + base_url: str | None = None, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> bytes: """Wrap async get requests.""" @@ -58,10 +58,10 @@ async def async_get_image( url = (base_url or self.base_url) + endpoint async with self.websession.get( - url, - **req_args, # type: ignore - headers=headers, - timeout=timeout, + url, + **req_args, # type: ignore + headers=headers, + timeout=timeout, ) as resp: resp_content = await resp.read() @@ -75,11 +75,11 @@ async def async_get_image( ) async def async_post_api_request( - self, - endpoint: str, - base_url: str | None = None, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + endpoint: str, + base_url: str | None = None, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> ClientResponse: """Wrap async post requests.""" @@ -90,11 +90,11 @@ async def async_post_api_request( ) async def async_get_api_request( - self, - endpoint: str, - base_url: str | None = None, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + endpoint: str, + base_url: str | None = None, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> ClientResponse: """Wrap async post requests.""" @@ -105,10 +105,10 @@ async def async_get_api_request( ) async def async_get_request( - self, - url: str, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + url: str, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> ClientResponse: """Wrap async post requests.""" @@ -118,18 +118,18 @@ async def async_get_request( req_args = self.prepare_request_get_arguments(params) async with self.websession.get( - url, - **req_args, - headers=headers, - timeout=timeout, + url, + **req_args, + headers=headers, + timeout=timeout, ) as resp: return await self.process_response(resp, url) async def async_post_request( - self, - url: str, - params: dict[str, Any] | None = None, - timeout: int = 5, + self, + url: str, + params: dict[str, Any] | None = None, + timeout: int = 5, ) -> ClientResponse: """Wrap async post requests.""" @@ -139,10 +139,10 @@ async def async_post_request( req_args = self.prepare_request_arguments(params) async with self.websession.post( - url, - **req_args, - headers=headers, - timeout=timeout, + url, + **req_args, + headers=headers, + timeout=timeout, ) as resp: return await self.process_response(resp, url) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 826a5168..580ccd0a 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -126,7 +126,7 @@ def _handle_schedules(self, raw_data): for t in timetable: time = ( - t.m_offset * 60 + t.m_offset * 60 ) # m_offset is in minute from the begininng of the day if len(self.energy_schedule_vals) == 0: time = 0 @@ -234,9 +234,9 @@ async def update(self, raw_data: RawData) -> bool: ) if ( - num_errors > 0 - and has_one_module_reachable is False - and has_an_update is False + num_errors > 0 + and has_one_module_reachable is False + and has_an_update is False ): return False @@ -287,10 +287,10 @@ def get_away_temp(self) -> float | None: return schedule.away_temp async def async_set_thermmode( - self, - mode: str, - end_time: int | None = None, - schedule_id: str | None = None, + self, + mode: str, + end_time: int | None = None, + schedule_id: str | None = None, ) -> bool: """Set thermotat mode.""" if schedule_id is not None and not self.is_valid_schedule(schedule_id): @@ -341,8 +341,8 @@ async def async_set_state(self, data: dict[str, Any]) -> bool: return (await resp.json()).get("status") == "ok" async def async_set_persons_home( - self, - person_ids: list[str] | None = None, + self, + person_ids: list[str] | None = None, ) -> ClientResponse: """Mark persons as home.""" post_params: dict[str, Any] = {"home_id": self.entity_id} @@ -354,8 +354,8 @@ async def async_set_persons_home( ) async def async_set_persons_away( - self, - person_id: str | None = None, + self, + person_id: str | None = None, ) -> ClientResponse: """Mark a person as away or set the whole home to being empty.""" @@ -368,9 +368,9 @@ async def async_set_persons_away( ) async def async_set_schedule_temperatures( - self, - zone_id: int, - temps: dict[str, int], + self, + zone_id: int, + temps: dict[str, int], ) -> None: """Set the scheduled room temperature for the given schedule ID.""" @@ -418,9 +418,9 @@ async def async_set_schedule_temperatures( await self.async_sync_schedule(selected_schedule.entity_id, schedule) async def async_sync_schedule( - self, - schedule_id: str, - schedule: dict[str, Any], + self, + schedule_id: str, + schedule: dict[str, Any], ) -> None: """Modify an existing schedule.""" if not is_valid_schedule(schedule): diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index b0950ab8..5d245a27 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -79,9 +79,9 @@ def update_topology(self, raw_data: RawData) -> None: self._update_attributes(raw_data) if ( - self.bridge - and self.bridge in self.home.modules - and getattr(self, "device_category") == "weather" + self.bridge + and self.bridge in self.home.modules + and getattr(self, "device_category") == "weather" ): self.name = update_name(self.name, self.home.modules[self.bridge].name) @@ -169,8 +169,8 @@ class Place: location: Location | None def __init__( - self, - data: dict[str, Any], + self, + data: dict[str, Any], ) -> None: """Initialize self.""" diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index a2c122ae..9800d7f4 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -641,7 +641,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) d_nrj_wh = dt_h * ( - min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w + min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w ) delta_energy += d_nrj_wh @@ -649,7 +649,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): return delta_energy def get_sum_energy_elec_power_adapted( - self, to_ts: int | float | None = None, conservative: bool = False + self, to_ts: int | float | None = None, conservative: bool = False ): """Compute proper energy value with adaptation from power.""" v = self.sum_energy_elec @@ -667,16 +667,16 @@ def get_sum_energy_elec_power_adapted( from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus if ( - from_ts is not None - and from_ts < to_ts - and isinstance(self, PowerMixin) - and isinstance(self, NetatmoBase) + from_ts is not None + and from_ts < to_ts + and isinstance(self, PowerMixin) + and isinstance(self, NetatmoBase) ): power_data = self.get_history_data( "power", from_ts=from_ts, to_ts=to_ts ) if isinstance( - self, EnergyHistoryMixin + self, EnergyHistoryMixin ): # well to please the linter.... delta_energy = self.compute_rieman_sum(power_data, conservative) @@ -705,11 +705,11 @@ def update_measures_num_calls(self): return len(self.home.energy_endpoints) async def async_update_measures( - self, - start_time: int | None = None, - end_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, + self, + start_time: int | None = None, + end_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7, ) -> int | None: """Update historical data.""" @@ -793,15 +793,15 @@ async def async_update_measures( return num_calls async def _prepare_exported_historical_data( - self, - start_time, - end_time, - delta_range, - hist_good_vals, - prev_end_time, - prev_start_time, - prev_sum_energy_elec, - peak_off_peak_mode, + self, + start_time, + end_time, + delta_range, + hist_good_vals, + prev_end_time, + prev_start_time, + prev_sum_energy_elec, + peak_off_peak_mode, ): computed_start = 0 computed_end = 0 @@ -842,8 +842,8 @@ async def _prepare_exported_historical_data( }, ) if ( - prev_sum_energy_elec is not None - and prev_sum_energy_elec > self.sum_energy_elec + prev_sum_energy_elec is not None + and prev_sum_energy_elec > self.sum_energy_elec ): msg = ( "ENERGY GOING DOWN %s from: %s to %s " @@ -882,14 +882,14 @@ async def _prepare_exported_historical_data( ) async def _get_aligned_energy_values_and_mode( - self, - start_time, - end_time, - delta_range, - energy_schedule_vals, - peak_off_peak_mode, - raw_datas, - data_points, + self, + start_time, + end_time, + delta_range, + energy_schedule_vals, + peak_off_peak_mode, + raw_datas, + data_points, ): hist_good_vals = [] for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): @@ -943,8 +943,8 @@ async def _get_aligned_energy_values_and_mode( ) if ( - self.home.energy_schedule_vals[idx_limit][1] - != cur_peak_or_off_peak_mode + self.home.energy_schedule_vals[idx_limit][1] + != cur_peak_or_off_peak_mode ): # we are NOT in a proper schedule time for this time span ... @@ -966,8 +966,8 @@ async def _get_aligned_energy_values_and_mode( else: # by construction of the energy schedule the next one should be of opposite mode if ( - energy_schedule_vals[idx_limit + 1][1] - != cur_peak_or_off_peak_mode + energy_schedule_vals[idx_limit + 1][1] + != cur_peak_or_off_peak_mode ): self._log_energy_error( start_time, @@ -983,12 +983,12 @@ async def _get_aligned_energy_values_and_mode( start_time_to_get_closer = energy_schedule_vals[ idx_limit + 1 - ][0] + ][0] diff_t = start_time_to_get_closer - srt_mid cur_start_time = ( - day_origin - + srt_beg - + (diff_t // interval_sec + 1) * interval_sec + day_origin + + srt_beg + + (diff_t // interval_sec + 1) * interval_sec ) hist_good_vals.append( @@ -1000,7 +1000,7 @@ async def _get_aligned_energy_values_and_mode( return hist_good_vals async def _compute_proper_energy_schedule_offsets( - self, start_time, end_time, interval_sec, raw_datas, data_points + self, start_time, end_time, interval_sec, raw_datas, data_points ): max_interval_sec = interval_sec for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): @@ -1172,4 +1172,5 @@ class Fan(FirmwareMixin, FanSpeedMixin, PowerMixin, Module): ... + # pylint: enable=too-many-ancestors diff --git a/src/pyatmo/modules/netatmo.py b/src/pyatmo/modules/netatmo.py index 98d4be90..d40f9350 100644 --- a/src/pyatmo/modules/netatmo.py +++ b/src/pyatmo/modules/netatmo.py @@ -225,13 +225,13 @@ class PublicWeatherArea: modules: list[dict[str, Any]] def __init__( - self, - lat_ne: str, - lon_ne: str, - lat_sw: str, - lon_sw: str, - required_data_type: str | None = None, - filtering: bool = False, + self, + lat_ne: str, + lon_ne: str, + lat_sw: str, + lon_sw: str, + required_data_type: str | None = None, + filtering: bool = False, ) -> None: """Initialize self.""" @@ -303,10 +303,10 @@ def get_latest_station_measures(self, data_type: str) -> dict[str, Any]: for station in self.modules: for module in station["measures"].values(): if ( - "type" in module - and data_type in module["type"] - and "res" in module - and module["res"] + "type" in module + and data_type in module["type"] + and "res" in module + and module["res"] ): measure_index = module["type"].index(data_type) latest_timestamp = sorted(module["res"], reverse=True)[0] diff --git a/src/pyatmo/room.py b/src/pyatmo/room.py index 337049f7..f6229081 100644 --- a/src/pyatmo/room.py +++ b/src/pyatmo/room.py @@ -39,10 +39,10 @@ class Room(NetatmoBase): therm_setpoint_end_time: int | None = None def __init__( - self, - home: Home, - room: dict[str, Any], - all_modules: dict[str, Module], + self, + home: Home, + room: dict[str, Any], + all_modules: dict[str, Module], ) -> None: """Initialize a Netatmo room instance.""" @@ -99,9 +99,9 @@ def update(self, raw_data: RawData) -> None: self.therm_setpoint_end_time = raw_data.get("therm_setpoint_end_time") async def async_therm_manual( - self, - temp: float | None = None, - end_time: int | None = None, + self, + temp: float | None = None, + end_time: int | None = None, ) -> None: """Set room temperature set point to manual.""" @@ -118,17 +118,17 @@ async def async_therm_frostguard(self, end_time: int | None = None) -> None: await self.async_therm_set(FROSTGUARD, end_time=end_time) async def async_therm_set( - self, - mode: str, - temp: float | None = None, - end_time: int | None = None, + self, + mode: str, + temp: float | None = None, + end_time: int | None = None, ) -> None: """Set room temperature set point.""" mode = MODE_MAP.get(mode, mode) if "NATherm1" in self.device_types or ( - "NRV" in self.device_types and not self.home.has_otm() + "NRV" in self.device_types and not self.home.has_otm() ): await self._async_set_thermpoint(mode, temp, end_time) @@ -136,10 +136,10 @@ async def async_therm_set( await self._async_therm_set(mode, temp, end_time) async def _async_therm_set( - self, - mode: str, - temp: float | None = None, - end_time: int | None = None, + self, + mode: str, + temp: float | None = None, + end_time: int | None = None, ) -> bool: """Set room temperature set point (OTM).""" @@ -161,10 +161,10 @@ async def _async_therm_set( return await self.home.async_set_state(json_therm_set) async def _async_set_thermpoint( - self, - mode: str, - temp: float | None = None, - end_time: int | None = None, + self, + mode: str, + temp: float | None = None, + end_time: int | None = None, ) -> None: """Set room temperature set point (NRV, NATherm1).""" diff --git a/tests/common.py b/tests/common.py index a6489984..3bff5404 100644 --- a/tests/common.py +++ b/tests/common.py @@ -67,7 +67,7 @@ async def fake_post_request(*args, **kwargs): ) else: - postfix = kwargs.get("POSTFIX", None) + postfix = kwargs.get("POSTFIX", None) if postfix is not None: payload = json.loads(load_fixture(f"{endpoint}_{postfix}.json")) else: diff --git a/tests/conftest.py b/tests/conftest.py index 1f25d71e..de0d56fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,11 +46,12 @@ async def async_home(async_account): yield async_account.homes[home_id] - @pytest.fixture(scope="function") async def async_account_multi(async_auth): """AsyncAccount fixture.""" - account = pyatmo.AsyncAccount(async_auth, support_only_homes=["aaaaaaaaaaabbbbbbbbbbccc"]) + account = pyatmo.AsyncAccount( + async_auth, support_only_homes=["aaaaaaaaaaabbbbbbbbbbccc"] + ) with patch( "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", diff --git a/tests/test_energy.py b/tests/test_energy.py index f5ec5dd8..35dd9797 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -40,12 +40,24 @@ async def test_historical_data_retrieval(async_account): await async_account.async_update_measures(home_id=home_id, module_id=module_id) # changed teh reference here as start and stop data was not calculated in the spirit of the netatmo api where their time data is in the fact representing the "middle" of the range and not the begining - assert module.historical_data[0] == {"Wh": 197, "duration": 60, "endTime": "2022-02-05T08:59:49Z", - "endTimeUnix": 1644051589, "energyMode": "standard", - "startTime": "2022-02-05T07:59:50Z", "startTimeUnix": 1644047989} - assert module.historical_data[-1] == {"Wh": 259, "duration": 60, "endTime": "2022-02-12T07:59:49Z", - "endTimeUnix": 1644652789, "energyMode": "standard", - "startTime": "2022-02-12T06:59:50Z", "startTimeUnix": 1644649189} + assert module.historical_data[0] == { + "Wh": 197, + "duration": 60, + "endTime": "2022-02-05T08:59:49Z", + "endTimeUnix": 1644051589, + "energyMode": "standard", + "startTime": "2022-02-05T07:59:50Z", + "startTimeUnix": 1644047989, + } + assert module.historical_data[-1] == { + "Wh": 259, + "duration": 60, + "endTime": "2022-02-12T07:59:49Z", + "endTimeUnix": 1644652789, + "energyMode": "standard", + "startTime": "2022-02-12T06:59:50Z", + "startTimeUnix": 1644649189, + } assert len(module.historical_data) == 168 @@ -63,23 +75,39 @@ async def test_historical_data_retrieval_multi(async_account_multi): strt = int(dt.datetime.fromisoformat("2024-03-03 00:10:00").timestamp()) end_time = int(dt.datetime.fromisoformat("2024-03-05 23:59:59").timestamp()) - await async_account_multi.async_update_measures(home_id=home_id, - module_id=module_id, - interval=MeasureInterval.HALF_HOUR, - start_time=strt, - end_time=end_time - ) + await async_account_multi.async_update_measures( + home_id=home_id, + module_id=module_id, + interval=MeasureInterval.HALF_HOUR, + start_time=strt, + end_time=end_time, + ) assert isinstance(module, EnergyHistoryMixin) - assert module.historical_data[0] == {"Wh": 0, "duration": 30, "endTime": "2024-03-02T23:40:00Z", - "endTimeUnix": 1709422800, "energyMode": "peak", - "startTime": "2024-03-02T23:10:01Z", "startTimeUnix": 1709421000} - assert module.historical_data[-1] == {"Wh": 0, "duration": 30, "endTime": "2024-03-05T23:10:00Z", - "endTimeUnix": 1709680200, "energyMode": "peak", - "startTime": "2024-03-05T22:40:01Z", "startTimeUnix": 1709678400} + assert module.historical_data[0] == { + "Wh": 0, + "duration": 30, + "endTime": "2024-03-02T23:40:00Z", + "endTimeUnix": 1709422800, + "energyMode": "peak", + "startTime": "2024-03-02T23:10:01Z", + "startTimeUnix": 1709421000, + } + assert module.historical_data[-1] == { + "Wh": 0, + "duration": 30, + "endTime": "2024-03-05T23:10:00Z", + "endTimeUnix": 1709680200, + "energyMode": "peak", + "startTime": "2024-03-05T22:40:01Z", + "startTimeUnix": 1709678400, + } assert len(module.historical_data) == 134 - assert module.sum_energy_elec == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak + assert ( + module.sum_energy_elec + == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak + ) assert module.sum_energy_elec_off_peak == 11219 assert module.sum_energy_elec_peak == 31282 @@ -98,22 +126,38 @@ async def test_historical_data_retrieval_multi_2(async_account_multi): strt = int(dt.datetime.fromisoformat("2024-03-15 00:29:51").timestamp()) end = int(dt.datetime.fromisoformat("2024-03-15 13:45:24").timestamp()) - await async_account_multi.async_update_measures(home_id=home_id, - module_id=module_id, - interval=MeasureInterval.HALF_HOUR, - start_time=strt, - end_time=end - ) - - assert module.historical_data[0] == {"Wh": 0, "duration": 30, "endTime": "2024-03-14T23:59:51Z", - "endTimeUnix": 1710460791, "energyMode": "peak", - "startTime": "2024-03-14T23:29:52Z", "startTimeUnix": 1710458991} - assert module.historical_data[-1] == {"Wh": 0, "duration": 30, "endTime": "2024-03-15T12:59:51Z", - "endTimeUnix": 1710507591, "energyMode": "peak", - "startTime": "2024-03-15T12:29:52Z", "startTimeUnix": 1710505791} + await async_account_multi.async_update_measures( + home_id=home_id, + module_id=module_id, + interval=MeasureInterval.HALF_HOUR, + start_time=strt, + end_time=end, + ) + + assert module.historical_data[0] == { + "Wh": 0, + "duration": 30, + "endTime": "2024-03-14T23:59:51Z", + "endTimeUnix": 1710460791, + "energyMode": "peak", + "startTime": "2024-03-14T23:29:52Z", + "startTimeUnix": 1710458991, + } + assert module.historical_data[-1] == { + "Wh": 0, + "duration": 30, + "endTime": "2024-03-15T12:59:51Z", + "endTimeUnix": 1710507591, + "energyMode": "peak", + "startTime": "2024-03-15T12:29:52Z", + "startTimeUnix": 1710505791, + } assert len(module.historical_data) == 26 - assert module.sum_energy_elec == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak + assert ( + module.sum_energy_elec + == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak + ) assert module.sum_energy_elec_off_peak == 780 assert module.sum_energy_elec_peak == 890 @@ -123,15 +167,15 @@ async def test_disconnected_main_bridge(async_account_multi): home_id = "aaaaaaaaaaabbbbbbbbbbccc" with open( - "fixtures/home_multi_status_error_disconnected.json", - encoding="utf-8", + "fixtures/home_multi_status_error_disconnected.json", + encoding="utf-8", ) as json_file: home_status_fixture = json.load(json_file) mock_home_status_resp = MockResponse(home_status_fixture, 200) with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_home_status_resp), + "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", + AsyncMock(return_value=mock_home_status_resp), ) as mock_request: try: await async_account_multi.async_update_status(home_id) diff --git a/tests/test_home.py b/tests/test_home.py index 81e81108..b4772a5b 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -145,7 +145,6 @@ async def test_home_event_update(async_account): assert events[1].event_type == "connection" - def test_device_types_missing(): """Test handling of missing device types.""" diff --git a/tests/testing_main_template.py b/tests/testing_main_template.py index 986b22a9..e3fe3183 100644 --- a/tests/testing_main_template.py +++ b/tests/testing_main_template.py @@ -7,9 +7,9 @@ from pyatmo.const import MeasureInterval - MY_TOKEN_FROM_NETATMO = "MY_TOKEN" + class MyAuth(AbstractAsyncAuth): async def async_get_access_token(self): @@ -30,24 +30,21 @@ async def main(): await account.async_update_status(home_id=home_id) - strt = 1709766000 + 10*60#1709421000+15*60 - end = 1709852400+10*60 - await account.async_update_measures(home_id=home_id, - module_id=module_id, - interval=MeasureInterval.HALF_HOUR, - start_time=strt, - end_time=end - ) - + strt = 1709766000 + 10 * 60 # 1709421000+15*60 + end = 1709852400 + 10 * 60 + await account.async_update_measures( + home_id=home_id, + module_id=module_id, + interval=MeasureInterval.HALF_HOUR, + start_time=strt, + end_time=end, + ) print(account) - if __name__ == "__main__": - topology = asyncio.run(main()) + topology = asyncio.run(main()) print(topology) - - From 4fe29876bae1d91799aaf84948f49ea292788ff6 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 31 May 2024 15:26:41 +0200 Subject: [PATCH 66/97] Cleaned supported homes with a way simpler approach, thx @cgtobi --- src/pyatmo/account.py | 57 ++++++++++--------------------------------- tests/conftest.py | 4 +-- 2 files changed, 15 insertions(+), 46 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 7a6b0bc8..5d7762be 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -2,7 +2,6 @@ from __future__ import annotations -import copy import logging from typing import TYPE_CHECKING, Any from uuid import uuid4 @@ -34,19 +33,11 @@ class AsyncAccount: """Async class of a Netatmo account.""" - def __init__( - self, - auth: AbstractAsyncAuth, - favorite_stations: bool = True, - support_only_homes: list | None = None, - ) -> None: + def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True) -> None: """Initialize the Netatmo account.""" self.auth: AbstractAsyncAuth = auth self.user: str | None = None - self.support_only_homes = support_only_homes - self.all_account_homes: dict[str, Home] = {} - self.additional_public_homes: dict[str, Home] = {} self.homes: dict[str, Home] = {} self.raw_data: RawData = {} self.favorite_stations: bool = favorite_stations @@ -60,38 +51,19 @@ def __repr__(self) -> str: f"{self.__class__.__name__}(user={self.user}, home_ids={self.homes.keys()}" ) - def update_supported_homes(self, support_only_homes: list | None = None): - """Update the exposed/supported homes.""" - - if support_only_homes is None or len(support_only_homes) == 0: - self.homes = copy.copy(self.all_account_homes) - else: - self.homes = {} - for h_id in support_only_homes: - h = self.all_account_homes.get(h_id) - if h is not None: - self.homes[h_id] = h - - if len(self.homes) == 0: - self.homes = copy.copy(self.all_account_homes) - - self.support_only_homes = list(self.homes) - - self.homes.update(self.additional_public_homes) - - def process_topology(self) -> None: + def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None: """Process topology information from /homesdata.""" for home in self.raw_data["homes"]: - if (home_id := home["id"]) in self.all_account_homes: - self.all_account_homes[home_id].update_topology(home) + if disabled_homes_ids and home["id"] in disabled_homes_ids: + continue + if (home_id := home["id"]) in self.homes: + self.homes[home_id].update_topology(home) else: - self.all_account_homes[home_id] = Home(self.auth, raw_data=home) - - self.update_supported_homes(self.support_only_homes) + self.homes[home_id] = Home(self.auth, raw_data=home) - async def async_update_topology(self) -> int: - """Retrieve topology data from /homesdata. Returns the number of performed API calls.""" + async def async_update_topology(self, disabled_homes_ids: list[str] | None = None) -> int: + """Retrieve topology data from /homesdata.""" resp = await self.auth.async_post_api_request( endpoint=GETHOMESDATA_ENDPOINT, @@ -100,7 +72,7 @@ async def async_update_topology(self) -> int: self.user = self.raw_data.get("user", {}).get("email") - self.process_topology() + self.process_topology(disabled_homes_ids=disabled_homes_ids) return 1 @@ -108,7 +80,6 @@ async def async_update_status(self, home_id: str | None = None) -> int: """Retrieve status data from /homestatus. Returns the number of performed API calls.""" if home_id is None: - self.update_supported_homes(self.support_only_homes) homes = self.homes else: homes = [home_id] @@ -163,9 +134,9 @@ async def async_update_measures( home_id: str, module_id: str, start_time: int | None = None, - interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7, end_time: int | None = None, + interval: MeasureInterval = MeasureInterval.HOUR, + days: int = 7 ) -> int: """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" @@ -273,7 +244,7 @@ async def update_devices( modules_data.append(normalize_weather_attributes(module_data)) modules_data.append(normalize_weather_attributes(device_data)) - self.additional_public_homes[home_id] = Home( + self.homes[home_id] = Home( self.auth, raw_data={ "id": home_id, @@ -281,8 +252,6 @@ async def update_devices( "modules": modules_data, }, ) - self.update_supported_homes(self.support_only_homes) - await self.homes[home_id].update( {HOME: {"modules": [normalize_weather_attributes(device_data)]}}, ) diff --git a/tests/conftest.py b/tests/conftest.py index de0d56fb..b341c0ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -50,7 +50,7 @@ async def async_home(async_account): async def async_account_multi(async_auth): """AsyncAccount fixture.""" account = pyatmo.AsyncAccount( - async_auth, support_only_homes=["aaaaaaaaaaabbbbbbbbbbccc"] + async_auth ) with patch( @@ -60,7 +60,7 @@ async def async_account_multi(async_auth): "pyatmo.auth.AbstractAsyncAuth.async_post_request", fake_post_request_multi, ): - await account.async_update_topology() + await account.async_update_topology(disabled_homes_ids=["eeeeeeeeeffffffffffaaaaa"]) yield account From 8d6a9bc0666735c730b6506177d268969182dcbc Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 31 May 2024 15:34:31 +0200 Subject: [PATCH 67/97] keep black API --- src/pyatmo/account.py | 6 ++++-- tests/conftest.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 5d7762be..041c3a33 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -62,7 +62,9 @@ def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None: else: self.homes[home_id] = Home(self.auth, raw_data=home) - async def async_update_topology(self, disabled_homes_ids: list[str] | None = None) -> int: + async def async_update_topology( + self, disabled_homes_ids: list[str] | None = None + ) -> int: """Retrieve topology data from /homesdata.""" resp = await self.auth.async_post_api_request( @@ -136,7 +138,7 @@ async def async_update_measures( start_time: int | None = None, end_time: int | None = None, interval: MeasureInterval = MeasureInterval.HOUR, - days: int = 7 + days: int = 7, ) -> int: """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" diff --git a/tests/conftest.py b/tests/conftest.py index b341c0ce..bca1108e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,7 +60,9 @@ async def async_account_multi(async_auth): "pyatmo.auth.AbstractAsyncAuth.async_post_request", fake_post_request_multi, ): - await account.async_update_topology(disabled_homes_ids=["eeeeeeeeeffffffffffaaaaa"]) + await account.async_update_topology( + disabled_homes_ids=["eeeeeeeeeffffffffffaaaaa"] + ) yield account From ec038bac2a62dba04dfb9af3ca57939808d13619 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 31 May 2024 15:38:45 +0200 Subject: [PATCH 68/97] keep black API --- src/pyatmo/account.py | 2 +- tests/conftest.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 041c3a33..07930c66 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -63,7 +63,7 @@ def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None: self.homes[home_id] = Home(self.auth, raw_data=home) async def async_update_topology( - self, disabled_homes_ids: list[str] | None = None + self, disabled_homes_ids: list[str] | None = None ) -> int: """Retrieve topology data from /homesdata.""" diff --git a/tests/conftest.py b/tests/conftest.py index bca1108e..bd9bfdea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,9 +49,7 @@ async def async_home(async_account): @pytest.fixture(scope="function") async def async_account_multi(async_auth): """AsyncAccount fixture.""" - account = pyatmo.AsyncAccount( - async_auth - ) + account = pyatmo.AsyncAccount(async_auth) with patch( "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", From 415fcf77608b965e2863a991a3edc394c7433c5d Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 4 Jun 2024 16:03:30 +0200 Subject: [PATCH 69/97] Added back a list of available homes to be able to select the one needed or not --- src/pyatmo/account.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 07930c66..455bfc96 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -38,6 +38,7 @@ def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True) -> N self.auth: AbstractAsyncAuth = auth self.user: str | None = None + self.all_account_homes_id: dict[str, str] = {} self.homes: dict[str, Home] = {} self.raw_data: RawData = {} self.favorite_stations: bool = favorite_stations @@ -55,9 +56,17 @@ def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None: """Process topology information from /homesdata.""" for home in self.raw_data["homes"]: - if disabled_homes_ids and home["id"] in disabled_homes_ids: + + home_id = home.get("id", "Unknown") + home_name = home.get("name", "Unknown") + self.all_account_homes_id[home_id] = home_name + + if disabled_homes_ids and home_id in disabled_homes_ids: + if home_id in self.homes: + del self.homes[home_id] continue - if (home_id := home["id"]) in self.homes: + + if home_id in self.homes: self.homes[home_id].update_topology(home) else: self.homes[home_id] = Home(self.auth, raw_data=home) From bae395960a9cc92fff66bc9cf191317f48b6e865 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 4 Jun 2024 17:03:50 +0200 Subject: [PATCH 70/97] white space --- src/pyatmo/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 455bfc96..e4a2a158 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -57,7 +57,7 @@ def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None: for home in self.raw_data["homes"]: - home_id = home.get("id", "Unknown") + home_id = home.get("id", "Unknown") home_name = home.get("name", "Unknown") self.all_account_homes_id[home_id] = home_name From d4bb6e4e5af8229dd79181ea6a9fb2637b6ca8e8 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 4 Jul 2024 22:41:24 +0200 Subject: [PATCH 71/97] Merge remote-tracking branch 'refs/remotes/upstream/development' into development # Conflicts: # src/pyatmo/modules/bticino.py --- .pre-commit-config.yaml | 10 +++++----- src/pyatmo/modules/base_class.py | 1 + src/pyatmo/room.py | 8 +++++++- tests/test_shutter.py | 6 ++++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41f7cbd9..165c750b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,14 +5,14 @@ exclude: ^(fixtures/) repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.4.9 hooks: - id: ruff args: - --fix - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade args: [--py310-plus] @@ -31,13 +31,13 @@ repos: - id: yesqa - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.4.2 hooks: - id: black language_version: python3 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.10.0 hooks: - id: mypy name: mypy @@ -46,7 +46,7 @@ repos: - types-requests - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 # Use the ref you want to point at + rev: v4.6.0 # Use the ref you want to point at hooks: - id: check-ast - id: no-commit-to-branch diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 5d245a27..7274ca0c 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -30,6 +30,7 @@ "monitoring": lambda x, _: x.get("monitoring", False) == "on", "battery_level": lambda x, y: x.get("battery_vp", x.get("battery_level")), "place": lambda x, _: Place(x.get("place")), + "target_position__step": lambda x, _: x.get("target_position:step"), } diff --git a/src/pyatmo/room.py b/src/pyatmo/room.py index f6229081..28578bb0 100644 --- a/src/pyatmo/room.py +++ b/src/pyatmo/room.py @@ -85,13 +85,19 @@ def evaluate_device_type(self) -> None: elif "BNS" in self.device_types: self.climate_type = DeviceType.BNS self.features.add("humidity") + elif "BNTH" in self.device_types: + self.climate_type = DeviceType.BNTH def update(self, raw_data: RawData) -> None: """Update room data.""" self.heating_power_request = raw_data.get("heating_power_request") self.humidity = raw_data.get("humidity") - self.reachable = raw_data.get("reachable") + if self.climate_type == DeviceType.BNTH: + # BNTH is wired, so the room is always reachable + self.reachable = True + else: + self.reachable = raw_data.get("reachable") self.therm_measured_temperature = raw_data.get("therm_measured_temperature") self.therm_setpoint_mode = raw_data.get("therm_setpoint_mode") self.therm_setpoint_temperature = raw_data.get("therm_setpoint_temperature") diff --git a/tests/test_shutter.py b/tests/test_shutter.py index b09d5526..f1a26ee3 100644 --- a/tests/test_shutter.py +++ b/tests/test_shutter.py @@ -84,6 +84,12 @@ def gen_json_data(position): endpoint="api/setstate", ) + assert await module.async_move_to_preferred_position() + mock_resp.assert_awaited_with( + params=gen_json_data(-2), + endpoint="api/setstate", + ) + assert await module.async_set_target_position(47) mock_resp.assert_awaited_with( params=gen_json_data(47), From 55761ebe362ae1643d4df60499a5c04c13f90d49 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 4 Jul 2024 23:01:00 +0200 Subject: [PATCH 72/97] Support NLE "connected ecocometer" that is a bridge --- src/pyatmo/const.py | 3 +++ src/pyatmo/helpers.py | 4 ++-- src/pyatmo/home.py | 10 ++++++++++ src/pyatmo/modules/module.py | 22 +++++++++++++++++++++- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index bd87c22f..4485f632 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -128,6 +128,9 @@ class MeasureType(Enum): SUM_ENERGY_PRICE_BASIC = "sum_energy_buy_from_grid_price$0" SUM_ENERGY_PRICE_PEAK = "sum_energy_buy_from_grid_price$1" SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_buy_from_grid_price$2" + SUM_ENERGY_ELEC_BASIC_OLD = "sum_energy_elec$0" + SUM_ENERGY_ELEC_PEAK_OLD = "sum_energy_elec$1" + SUM_ENERGY_ELEC_OFF_PEAK_OLD = "sum_energy_elec$2" class MeasureInterval(Enum): diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 9db2adbd..8aa1dd6c 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -38,7 +38,7 @@ def extract_raw_data(resp: Any, tag: str) -> dict[str, Any]: return {"public": resp["body"], "errors": []} if resp is None or "body" not in resp or tag not in resp["body"]: - LOG.debug("Server response: %s", resp) + LOG.debug("Server response (tag: %s): %s", tag, resp) raise NoDevice("No device found, errors in response") if tag == "homes": @@ -48,7 +48,7 @@ def extract_raw_data(resp: Any, tag: str) -> dict[str, Any]: } if not (raw_data := fix_id(resp["body"].get(tag))): - LOG.debug("Server response: %s", resp) + LOG.debug("Server response (tag: %s): %s", tag, resp) raise NoDevice("No device data available") return {tag: raw_data, "errors": resp["body"].get("errors", [])} diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 580ccd0a..5772af1e 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -98,6 +98,7 @@ def _handle_schedules(self, raw_data): self.energy_schedule_vals = [] self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] + self.energy_endpoints_old = [MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value] if nrj_schedule is not None: # Tariff option (basic = always the same price, peak_and_off_peak = peak & off peak hours) @@ -107,6 +108,7 @@ def _handle_schedules(self, raw_data): if type_tariff == "peak_and_off_peak" and len(zones) >= 2: self.energy_endpoints = [None, None] + self.energy_endpoints_old = [None, None] self.energy_endpoints[ENERGY_ELEC_PEAK_IDX] = ( MeasureType.SUM_ENERGY_ELEC_PEAK.value @@ -115,6 +117,13 @@ def _handle_schedules(self, raw_data): MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value ) + self.energy_endpoints_old[ENERGY_ELEC_PEAK_IDX] = ( + MeasureType.SUM_ENERGY_ELEC_PEAK_OLD.value + ) + self.energy_endpoints_old[ENERGY_ELEC_OFF_IDX] = ( + MeasureType.SUM_ENERGY_ELEC_OFF_PEAK_OLD.value + ) + if zones[0].price_type == "peak": peak_id = zones[0].entity_id else: @@ -139,6 +148,7 @@ def _handle_schedules(self, raw_data): else: self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] + self.energy_endpoints_old = [MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value] def get_module(self, module: dict) -> Module: """Return module.""" diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 7fac79d9..8b042a04 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -641,7 +641,7 @@ def compute_rieman_sum(self, power_data, conservative: bool = False): """Compute energy from power with a rieman sum.""" delta_energy = 0 - if len(power_data) > 1: + if power_data and len(power_data) > 1: # compute a rieman sum, as best as possible , trapezoidal, taking pessimistic asumption # as we don't want to artifically go up the previous one @@ -1054,6 +1054,13 @@ async def _compute_proper_energy_schedule_offsets( async def _energy_API_calls(self, start_time, end_time, interval): num_calls = 0 data_points = self.home.energy_endpoints + + # when the bridge is a connected meter, use old endpoints + bridge_module = self.home.modules.get(self.bridge) + if bridge_module: + if bridge_module.device_type == DeviceType.NLE: + data_points =self.home.energy_endpoints_old + raw_datas = [] for data_point in data_points: @@ -1126,6 +1133,19 @@ async def update(self, raw_data: RawData) -> None: self.update_topology(raw_data) self.update_features() + # If we have an NLE as a bridge all its bridged modules will have to be reachable + if self.device_type == DeviceType.NLE: + # if there is a bridge it means it is a leaf + if self.bridge: + bridge_module = self.home.modules.get(self.bridge) + if bridge_module: + if bridge_module.device_type == DeviceType.NLE: + self.reachable = True + elif self.modules: + # this NLE is a bridge itself : make it not available + self.reachable = False + + if not self.reachable and self.modules: # Update bridged modules and associated rooms for module_id in self.modules: From 33fc1458c947af5ea76a75e86308a514377a8ef6 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 23 Jul 2024 22:47:08 +0200 Subject: [PATCH 73/97] Support NLE "connected ecocometer" that is a bridge...and has no power data in teh homestatus API --- src/pyatmo/modules/legrand.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 657c83cf..19016109 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -100,8 +100,8 @@ class NLPC(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): """Legrand / BTicino connected energy meter.""" -class NLE(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): - """Legrand / BTicino connected ecometer.""" +class NLE(FirmwareMixin, EnergyHistoryMixin, Module): + """Legrand / BTicino connected ecometer. no power supported for the NLE (in the home status API)""" class NLPS(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): From 8ed5bacffd1394fed71cc9b510040d72c037900f Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 23 Jul 2024 22:51:26 +0200 Subject: [PATCH 74/97] Support NLE "connected ecocometer" that is a bridge...and has no power data in teh homestatus API --- src/pyatmo/modules/legrand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 19016109..683863d8 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -101,7 +101,7 @@ class NLPC(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): class NLE(FirmwareMixin, EnergyHistoryMixin, Module): - """Legrand / BTicino connected ecometer. no power supported for the NLE (in the home status API)""" + """Legrand / BTicino connected ecometer. no power supported for the NLE (in the home status API).""" class NLPS(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): From d544802733db9dedebb0394b2a30ffefc7e468b0 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Tue, 23 Jul 2024 23:30:37 +0200 Subject: [PATCH 75/97] Support NLE "connected ecocometer" that is a bridge...and has no power data in teh homestatus API --- src/pyatmo/home.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 5772af1e..102515f0 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -173,10 +173,7 @@ def update_topology(self, raw_data: RawData) -> None: raw_modules = raw_data.get("modules", []) for module in raw_modules: if (module_id := module["id"]) not in self.modules: - self.modules[module_id] = getattr(modules, module["type"])( - home=self, - module=module, - ) + self.modules[module_id] = self.get_module(module) else: self.modules[module_id].update_topology(module) From c057ff2125a45d9e232909b9e72749e74f22c246 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 24 Jul 2024 00:39:07 +0200 Subject: [PATCH 76/97] Black / Rust --- src/pyatmo/home.py | 4 ++- src/pyatmo/modules/module.py | 53 ++++++++++++++++++++---------------- tests/test_home.py | 2 +- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 102515f0..0082eab1 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -98,7 +98,9 @@ def _handle_schedules(self, raw_data): self.energy_schedule_vals = [] self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] - self.energy_endpoints_old = [MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value] + self.energy_endpoints_old = [ + MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value + ] if nrj_schedule is not None: # Tariff option (basic = always the same price, peak_and_off_peak = peak & off peak hours) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 8b042a04..c5c0b2cd 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -778,7 +778,10 @@ async def async_update_measures( self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - self._last_energy_from_API_end_for_power_adjustment_calculus = start_time # no data at all: we know nothing for the end: best guess, it is the start + + # no data at all: we know nothing for the end: best guess, it is the start + self._last_energy_from_API_end_for_power_adjustment_calculus = start_time + self.in_reset = False if len(hist_good_vals) == 0: @@ -842,7 +845,9 @@ async def _prepare_exported_historical_data( if computed_start == 0: computed_start = c_start computed_end = c_end - computed_end_for_calculus = c_end # - delta_range #not sure, revert ... it seems the energy value effectively stops at those mid values + + # - delta_range not sure, revert ... it seems the energy value effectively stops at those mid values + computed_end_for_calculus = c_end # - delta_range start_time_string = f"{datetime.fromtimestamp(c_start + 1, tz=timezone.utc).isoformat().split('+')[0]}Z" end_time_string = f"{datetime.fromtimestamp(c_end, tz=timezone.utc).isoformat().split('+')[0]}Z" @@ -908,7 +913,7 @@ async def _get_aligned_energy_values_and_mode( data_points, ): hist_good_vals = [] - for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): + for cur_peak_off_peak_mode, values_lots in enumerate(raw_datas): for values_lot in values_lots: try: start_lot_time = int(values_lot["beg_time"]) @@ -916,13 +921,13 @@ async def _get_aligned_energy_values_and_mode( self._log_energy_error( start_time, end_time, - msg=f"beg_time missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode], + msg=f"beg_time missing {data_points[cur_peak_off_peak_mode]}", + body=raw_datas[cur_peak_off_peak_mode], ) raise ApiError( - f"Energy badly formed resp beg_time missing: {raw_datas[cur_peak_or_off_peak_mode]} - " + f"Energy badly formed resp beg_time missing: {raw_datas[cur_peak_off_peak_mode]} - " f"module: {self.name} - " - f"when accessing '{data_points[cur_peak_or_off_peak_mode]}'" + f"when accessing '{data_points[cur_peak_off_peak_mode]}'" ) from None interval_sec = values_lot.get("step_time") @@ -931,8 +936,8 @@ async def _get_aligned_energy_values_and_mode( self._log_energy_error( start_time, end_time, - msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode], + msg=f"step_time missing {data_points[cur_peak_off_peak_mode]}", + body=raw_datas[cur_peak_off_peak_mode], ) interval_sec = 2 * delta_range else: @@ -960,7 +965,7 @@ async def _get_aligned_energy_values_and_mode( if ( self.home.energy_schedule_vals[idx_limit][1] - != cur_peak_or_off_peak_mode + != cur_peak_off_peak_mode ): # we are NOT in a proper schedule time for this time span ... @@ -970,31 +975,31 @@ async def _get_aligned_energy_values_and_mode( self._log_energy_error( start_time, end_time, - msg=f"bad idx missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode], + msg=f"bad idx missing {data_points[cur_peak_off_peak_mode]}", + body=raw_datas[cur_peak_off_peak_mode], ) raise ApiError( - f"Energy badly formed bad schedule idx in vals: {raw_datas[cur_peak_or_off_peak_mode]} - " - f"module: {self.name} - " - f"when accessing '{data_points[cur_peak_or_off_peak_mode]}'" + f"Energy badly formed bad schedule idx in vals: {raw_datas[cur_peak_off_peak_mode]}" + f" - module: {self.name} - " + f"when accessing '{data_points[cur_peak_off_peak_mode]}'" ) else: # by construction of the energy schedule the next one should be of opposite mode if ( energy_schedule_vals[idx_limit + 1][1] - != cur_peak_or_off_peak_mode + != cur_peak_off_peak_mode ): self._log_energy_error( start_time, end_time, - msg=f"bad schedule {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode], + msg=f"bad schedule {data_points[cur_peak_off_peak_mode]}", + body=raw_datas[cur_peak_off_peak_mode], ) raise ApiError( - f"Energy badly formed bad schedule: {raw_datas[cur_peak_or_off_peak_mode]} - " + f"Energy badly formed bad schedule: {raw_datas[cur_peak_off_peak_mode]} - " f"module: {self.name} - " - f"when accessing '{data_points[cur_peak_or_off_peak_mode]}'" + f"when accessing '{data_points[cur_peak_off_peak_mode]}'" ) start_time_to_get_closer = energy_schedule_vals[ @@ -1008,7 +1013,7 @@ async def _get_aligned_energy_values_and_mode( ) hist_good_vals.append( - (cur_start_time, int(val), cur_peak_or_off_peak_mode) + (cur_start_time, int(val), cur_peak_off_peak_mode) ) cur_start_time = cur_start_time + interval_sec @@ -1059,7 +1064,7 @@ async def _energy_API_calls(self, start_time, end_time, interval): bridge_module = self.home.modules.get(self.bridge) if bridge_module: if bridge_module.device_type == DeviceType.NLE: - data_points =self.home.energy_endpoints_old + data_points = self.home.energy_endpoints_old raw_datas = [] for data_point in data_points: @@ -1142,8 +1147,8 @@ async def update(self, raw_data: RawData) -> None: if bridge_module.device_type == DeviceType.NLE: self.reachable = True elif self.modules: - # this NLE is a bridge itself : make it not available - self.reachable = False + # this NLE is a bridge itself : make it not available + self.reachable = False if not self.reachable and self.modules: diff --git a/tests/test_home.py b/tests/test_home.py index b4772a5b..f73723b0 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -1,6 +1,6 @@ """Define tests for home module.""" -import datetime as dt +# import datetime as dt import json from unittest.mock import AsyncMock, patch From 16af56aa6bc7ab1fc272d5900e53e381d4b6d8f0 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 24 Jul 2024 00:57:36 +0200 Subject: [PATCH 77/97] Black / Rust --- src/pyatmo/home.py | 4 +--- src/pyatmo/modules/module.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 0082eab1..102515f0 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -98,9 +98,7 @@ def _handle_schedules(self, raw_data): self.energy_schedule_vals = [] self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] - self.energy_endpoints_old = [ - MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value - ] + self.energy_endpoints_old = [MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value] if nrj_schedule is not None: # Tariff option (basic = always the same price, peak_and_off_peak = peak & off peak hours) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index c5c0b2cd..9ef7fef2 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -1150,7 +1150,6 @@ async def update(self, raw_data: RawData) -> None: # this NLE is a bridge itself : make it not available self.reachable = False - if not self.reachable and self.modules: # Update bridged modules and associated rooms for module_id in self.modules: From 9409e6b71a5afa9fd1416adc7dd32f110adff263 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 24 Jul 2024 01:05:02 +0200 Subject: [PATCH 78/97] Black / Rust --- src/pyatmo/modules/module.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 9ef7fef2..2578a1d3 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -922,10 +922,10 @@ async def _get_aligned_energy_values_and_mode( start_time, end_time, msg=f"beg_time missing {data_points[cur_peak_off_peak_mode]}", - body=raw_datas[cur_peak_off_peak_mode], + body=values_lots, ) raise ApiError( - f"Energy badly formed resp beg_time missing: {raw_datas[cur_peak_off_peak_mode]} - " + f"Energy badly formed resp beg_time missing: {values_lots} - " f"module: {self.name} - " f"when accessing '{data_points[cur_peak_off_peak_mode]}'" ) from None @@ -937,7 +937,7 @@ async def _get_aligned_energy_values_and_mode( start_time, end_time, msg=f"step_time missing {data_points[cur_peak_off_peak_mode]}", - body=raw_datas[cur_peak_off_peak_mode], + body=values_lots, ) interval_sec = 2 * delta_range else: @@ -976,12 +976,12 @@ async def _get_aligned_energy_values_and_mode( start_time, end_time, msg=f"bad idx missing {data_points[cur_peak_off_peak_mode]}", - body=raw_datas[cur_peak_off_peak_mode], + body=values_lots, ) raise ApiError( - f"Energy badly formed bad schedule idx in vals: {raw_datas[cur_peak_off_peak_mode]}" - f" - module: {self.name} - " + f"Energy badly formed bad schedule idx in vals: {values_lots} - " + f"module: {self.name} - " f"when accessing '{data_points[cur_peak_off_peak_mode]}'" ) else: @@ -994,10 +994,10 @@ async def _get_aligned_energy_values_and_mode( start_time, end_time, msg=f"bad schedule {data_points[cur_peak_off_peak_mode]}", - body=raw_datas[cur_peak_off_peak_mode], + body=values_lots, ) raise ApiError( - f"Energy badly formed bad schedule: {raw_datas[cur_peak_off_peak_mode]} - " + f"Energy badly formed bad schedule: {values_lots} - " f"module: {self.name} - " f"when accessing '{data_points[cur_peak_off_peak_mode]}'" ) @@ -1034,7 +1034,7 @@ async def _compute_proper_energy_schedule_offsets( start_time, end_time, msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", - body=raw_datas[cur_peak_or_off_peak_mode], + body=values_lots, ) else: local_step_time = int(local_step_time) From b4762576fec14aa690f0a1afa96275f39e66f1e0 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 24 Jul 2024 22:56:27 +0200 Subject: [PATCH 79/97] Remove schedule code API calls, etc ... to make it compatible with old lib --- ...y_from_grid$0_12_34_56_00_00_a1_4c_da.json | 517 ------------------ ...y_from_grid$1_98_76_54_32_10_00_00_49.json | 1 - ...y_from_grid$1_98_76_54_32_10_00_00_73.json | 286 ---------- ...y_from_grid$2_98_76_54_32_10_00_00_49.json | 1 - ...y_from_grid$2_98_76_54_32_10_00_00_73.json | 154 ------ ...y_from_grid$2_12_34_56_00_00_a1_4c_da.json | 517 ++++++++++++++++++ ...y_from_grid$2_98_76_54_32_10_00_00_69.json | 247 +++++++++ fixtures/homesdata_multi.json | 9 +- src/pyatmo/account.py | 76 +-- src/pyatmo/auth.py | 47 +- src/pyatmo/const.py | 49 +- src/pyatmo/helpers.py | 1 + src/pyatmo/home.py | 129 +---- src/pyatmo/modules/base_class.py | 1 + src/pyatmo/modules/device_types.py | 2 +- src/pyatmo/modules/legrand.py | 1 - src/pyatmo/modules/module.py | 377 +++++-------- src/pyatmo/modules/netatmo.py | 1 - src/pyatmo/schedule.py | 166 +----- tests/test_energy.py | 119 ++-- tests/testing_main_template.py | 3 +- 21 files changed, 1018 insertions(+), 1686 deletions(-) delete mode 100644 fixtures/getmeasure_sum_energy_buy_from_grid$0_12_34_56_00_00_a1_4c_da.json delete mode 100644 fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_49.json delete mode 100644 fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_73.json delete mode 100644 fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_49.json delete mode 100644 fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_73.json create mode 100644 fixtures/getmeasure_sum_energy_buy_from_grid,sum_energy_buy_from_grid$0,sum_energy_buy_from_grid$1,sum_energy_buy_from_grid$2_12_34_56_00_00_a1_4c_da.json create mode 100644 fixtures/getmeasure_sum_energy_buy_from_grid,sum_energy_buy_from_grid$0,sum_energy_buy_from_grid$1,sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_69.json diff --git a/fixtures/getmeasure_sum_energy_buy_from_grid$0_12_34_56_00_00_a1_4c_da.json b/fixtures/getmeasure_sum_energy_buy_from_grid$0_12_34_56_00_00_a1_4c_da.json deleted file mode 100644 index 9eb499ab..00000000 --- a/fixtures/getmeasure_sum_energy_buy_from_grid$0_12_34_56_00_00_a1_4c_da.json +++ /dev/null @@ -1,517 +0,0 @@ -{ - "body": [ - { - "beg_time": 1644049789, - "step_time": 3600, - "value": [ - [ - 197 - ], - [ - 262 - ], - [ - 168 - ], - [ - 219 - ], - [ - 224 - ], - [ - 299 - ], - [ - 343 - ], - [ - 443 - ], - [ - 317 - ], - [ - 229 - ], - [ - 147 - ], - [ - 178 - ], - [ - 340 - ], - [ - 536 - ], - [ - 471 - ], - [ - 397 - ], - [ - 290 - ], - [ - 302 - ], - [ - 309 - ], - [ - 219 - ], - [ - 154 - ], - [ - 163 - ], - [ - 146 - ], - [ - 189 - ], - [ - 256 - ], - [ - 162 - ], - [ - 1288 - ], - [ - 256 - ], - [ - 709 - ], - [ - 310 - ], - [ - 379 - ], - [ - 296 - ], - [ - 230 - ], - [ - 505 - ], - [ - 362 - ], - [ - 611 - ], - [ - 597 - ], - [ - 505 - ], - [ - 431 - ], - [ - 1538 - ], - [ - 265 - ], - [ - 187 - ], - [ - 162 - ], - [ - 150 - ], - [ - 155 - ], - [ - 147 - ], - [ - 211 - ], - [ - 211 - ], - [ - 272 - ], - [ - 271 - ], - [ - 331 - ], - [ - 180 - ], - [ - 184 - ], - [ - 182 - ], - [ - 232 - ], - [ - 288 - ], - [ - 266 - ], - [ - 256 - ], - [ - 249 - ], - [ - 372 - ], - [ - 379 - ], - [ - 585 - ], - [ - 387 - ], - [ - 277 - ], - [ - 223 - ], - [ - 202 - ], - [ - 163 - ], - [ - 143 - ], - [ - 158 - ], - [ - 145 - ], - [ - 232 - ], - [ - 231 - ], - [ - 160 - ], - [ - 261 - ], - [ - 376 - ], - [ - 216 - ], - [ - 202 - ], - [ - 295 - ], - [ - 310 - ], - [ - 235 - ], - [ - 188 - ], - [ - 269 - ], - [ - 250 - ], - [ - 334 - ], - [ - 434 - ], - [ - 353 - ], - [ - 279 - ], - [ - 266 - ], - [ - 226 - ], - [ - 179 - ], - [ - 149 - ], - [ - 146 - ], - [ - 143 - ], - [ - 136 - ], - [ - 173 - ], - [ - 221 - ], - [ - 190 - ], - [ - 177 - ], - [ - 290 - ], - [ - 352 - ], - [ - 252 - ], - [ - 284 - ], - [ - 173 - ], - [ - 165 - ], - [ - 144 - ], - [ - 175 - ], - [ - 268 - ], - [ - 363 - ], - [ - 544 - ], - [ - 515 - ], - [ - 525 - ], - [ - 431 - ], - [ - 225 - ], - [ - 183 - ], - [ - 178 - ], - [ - 155 - ], - [ - 170 - ], - [ - 156 - ], - [ - 169 - ], - [ - 226 - ], - [ - 255 - ], - [ - 273 - ], - [ - 466 - ], - [ - 406 - ], - [ - 333 - ], - [ - 194 - ], - [ - 234 - ], - [ - 271 - ], - [ - 238 - ], - [ - 221 - ], - [ - 205 - ], - [ - 258 - ], - [ - 430 - ], - [ - 446 - ], - [ - 390 - ], - [ - 306 - ], - [ - 223 - ], - [ - 165 - ], - [ - 154 - ], - [ - 147 - ], - [ - 155 - ], - [ - 140 - ], - [ - 153 - ], - [ - 228 - ], - [ - 237 - ], - [ - 201 - ], - [ - 183 - ], - [ - 194 - ], - [ - 135 - ], - [ - 206 - ], - [ - 215 - ], - [ - 147 - ], - [ - 159 - ], - [ - 185 - ], - [ - 168 - ], - [ - 257 - ], - [ - 262 - ], - [ - 141 - ], - [ - 151 - ], - [ - 157 - ], - [ - 133 - ], - [ - 147 - ], - [ - 135 - ], - [ - 139 - ], - [ - 136 - ], - [ - 127 - ], - [ - 169 - ], - [ - 259 - ] - ] - } - ], - "status": "ok", - "time_exec": 0.5533828735351562, - "time_server": 1647935044 -} diff --git a/fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_49.json b/fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_49.json deleted file mode 100644 index 0f60f8f8..00000000 --- a/fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_49.json +++ /dev/null @@ -1 +0,0 @@ -{"body":[{"beg_time":1710459891,"step_time":1800,"value":[[0],[0]]},{"beg_time":1710465291,"step_time":1800,"value":[[0],[222],[328],[333],[7],[0],[0],[0],[0],[0],[0],[0],[0],[0]]}],"status":"ok","time_exec":0.08218002319335938,"time_server":1710510905} \ No newline at end of file diff --git a/fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_73.json b/fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_73.json deleted file mode 100644 index c1d788b2..00000000 --- a/fixtures/getmeasure_sum_energy_buy_from_grid$1_98_76_54_32_10_00_00_73.json +++ /dev/null @@ -1,286 +0,0 @@ -{ - "body": [ - { - "beg_time": 1709421900, - "step_time": 1800, - "value": [ - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ] - ] - }, - { - "beg_time": 1709459700, - "step_time": 1800, - "value": [ - [ - 526 - ], - [ - 1194 - ], - [ - 1195 - ], - [ - 1176 - ], - [ - 1177 - ], - [ - 1164 - ], - [ - 1524 - ], - [ - 416 - ], - [ - 659 - ], - [ - 743 - ], - [ - 1033 - ], - [ - 1052 - ], - [ - 964 - ], - [ - 985 - ], - [ - 1031 - ], - [ - 924 - ], - [ - 73 - ], - [ - 156 - ], - [ - 320 - ], - [ - 268 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 768 - ], - [ - 912 - ], - [ - 889 - ], - [ - 399 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 38 - ], - [ - 38 - ], - [ - 0 - ], - [ - 218 - ], - [ - 186 - ], - [ - 250 - ], - [ - 363 - ], - [ - 479 - ], - [ - 579 - ], - [ - 680 - ], - [ - 832 - ], - [ - 872 - ], - [ - 699 - ], - [ - 37 - ], - [ - 77 - ], - [ - 248 - ], - [ - 209 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 418 - ], - [ - 690 - ], - [ - 714 - ], - [ - 327 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 173 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 91 - ], - [ - 212 - ], - [ - 199 - ], - [ - 161 - ], - [ - 291 - ], - [ - 394 - ], - [ - 617 - ], - [ - 688 - ], - [ - 526 - ], - [ - 428 - ], - [ - 0 - ] - ] - } - ], - "status": "ok", - "time_exec": 0.10495901107788086, - "time_server": 1709768260 -} \ No newline at end of file diff --git a/fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_49.json b/fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_49.json deleted file mode 100644 index 03784fcd..00000000 --- a/fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_49.json +++ /dev/null @@ -1 +0,0 @@ -{"body":[{"beg_time":1710465291,"step_time":1800,"value":[[0],[0],[0],[0],[0],[0],[0],[0],[332],[448]]}],"status":"ok","time_exec":0.02698206901550293,"time_server":1710511044} \ No newline at end of file diff --git a/fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_73.json b/fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_73.json deleted file mode 100644 index 10f8884a..00000000 --- a/fixtures/getmeasure_sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_73.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "body": [ - { - "beg_time": 1709421900, - "step_time": 1800, - "value": [ - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ] - ] - }, - { - "beg_time": 1709459700, - "step_time": 1800, - "value": [ - [ - 728 - ], - [ - 1196 - ], - [ - 1197 - ], - [ - 1206 - ], - [ - 1167 - ], - [ - 887 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 200 - ], - [ - 1532 - ], - [ - 165 - ], - [ - 252 - ], - [ - 282 - ], - [ - 195 - ], - [ - 307 - ], - [ - 396 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 201 - ], - [ - 1308 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ], - [ - 0 - ] - ] - } - ], - "status": "ok", - "time_exec": 0.13054180145263672, - "time_server": 1709768401 -} \ No newline at end of file diff --git a/fixtures/getmeasure_sum_energy_buy_from_grid,sum_energy_buy_from_grid$0,sum_energy_buy_from_grid$1,sum_energy_buy_from_grid$2_12_34_56_00_00_a1_4c_da.json b/fixtures/getmeasure_sum_energy_buy_from_grid,sum_energy_buy_from_grid$0,sum_energy_buy_from_grid$1,sum_energy_buy_from_grid$2_12_34_56_00_00_a1_4c_da.json new file mode 100644 index 00000000..1c1d1694 --- /dev/null +++ b/fixtures/getmeasure_sum_energy_buy_from_grid,sum_energy_buy_from_grid$0,sum_energy_buy_from_grid$1,sum_energy_buy_from_grid$2_12_34_56_00_00_a1_4c_da.json @@ -0,0 +1,517 @@ +{ + "body": [ + { + "beg_time": 1644049789, + "step_time": 3600, + "value": [ + [ null, + 197 + ,null, null], + [null, + 262 + ,null, null], + [null, + 168 + ,null, null], + [null, + 219 + ,null, null], + [null, + 224 + ,null, null], + [null, + 299 + ,null, null], + [null, + 343 + ,null, null], + [null, + 443 + ,null, null], + [null, + 317 + ,null, null], + [null, + 229 + ,null, null], + [null, + 147 + ,null, null], + [null, + 178 + ,null, null], + [null, + 340 + ,null, null], + [null, + 536 + ,null, null], + [null, + 471 + ,null, null], + [null, + 397 + ,null, null], + [null, + 290 + ,null, null], + [null, + 302 + ,null, null], + [null, + 309 + ,null, null], + [null, + 219 + ,null, null], + [null, + 154 + ,null, null], + [null, + 163 + ,null, null], + [null, + 146 + ,null, null], + [null, + 189 + ,null, null], + [null, + 256 + ,null, null], + [null, + 162 + ,null, null], + [null, + 1288 + ,null, null], + [null, + 256 + ,null, null], + [null, + 709 + ,null, null], + [null, + 310 + ,null, null], + [null, + 379 + ,null, null], + [null, + 296 + ,null, null], + [null, + 230 + ,null, null], + [null, + 505 + ,null, null], + [null, + 362 + ,null, null], + [null, + 611 + ,null, null], + [null, + 597 + ,null, null], + [null, + 505 + ,null, null], + [null, + 431 + ,null, null], + [null, + 1538 + ,null, null], + [null, + 265 + ,null, null], + [null, + 187 + ,null, null], + [null, + 162 + ,null, null], + [null, + 150 + ,null, null], + [null, + 155 + ,null, null], + [null, + 147 + ,null, null], + [null, + 211 + ,null, null], + [null, + 211 + ,null, null], + [null, + 272 + ,null, null], + [null, + 271 + ,null, null], + [null, + 331 + ,null, null], + [null, + 180 + ,null, null], + [null, + 184 + ,null, null], + [null, + 182 + ,null, null], + [null, + 232 + ,null, null], + [null, + 288 + ,null, null], + [null, + 266 + ,null, null], + [null, + 256 + ,null, null], + [null, + 249 + ,null, null], + [null, + 372 + ,null, null], + [null, + 379 + ,null, null], + [null, + 585 + ,null, null], + [null, + 387 + ,null, null], + [null, + 277 + ,null, null], + [null, + 223 + ,null, null], + [null, + 202 + ,null, null], + [null, + 163 + ,null, null], + [null, + 143 + ,null, null], + [null, + 158 + ,null, null], + [null, + 145 + ,null, null], + [null, + 232 + ,null, null], + [null, + 231 + ,null, null], + [null, + 160 + ,null, null], + [null, + 261 + ,null, null], + [null, + 376 + ,null, null], + [null, + 216 + ,null, null], + [null, + 202 + ,null, null], + [null, + 295 + ,null, null], + [null, + 310 + ,null, null], + [null, + 235 + ,null, null], + [null, + 188 + ,null, null], + [null, + 269 + ,null, null], + [null, + 250 + ,null, null], + [null, + 334 + ,null, null], + [null, + 434 + ,null, null], + [null, + 353 + ,null, null], + [null, + 279 + ,null, null], + [null, + 266 + ,null, null], + [null, + 226 + ,null, null], + [null, + 179 + ,null, null], + [null, + 149 + ,null, null], + [null, + 146 + ,null, null], + [null, + 143 + ,null, null], + [null, + 136 + ,null, null], + [null, + 173 + ,null, null], + [null, + 221 + ,null, null], + [null, + 190 + ,null, null], + [null, + 177 + ,null, null], + [null, + 290 + ,null, null], + [null, + 352 + ,null, null], + [null, + 252 + ,null, null], + [null, + 284 + ,null, null], + [null, + 173 + ,null, null], + [null, + 165 + ,null, null], + [null, + 144 + ,null, null], + [null, + 175 + ,null, null], + [null, + 268 + ,null, null], + [null, + 363 + ,null, null], + [null, + 544 + ,null, null], + [null, + 515 + ,null, null], + [null, + 525 + ,null, null], + [null, + 431 + ,null, null], + [null, + 225 + ,null, null], + [null, + 183 + ,null, null], + [null, + 178 + ,null, null], + [null, + 155 + ,null, null], + [null, + 170 + ,null, null], + [null, + 156 + ,null, null], + [null, + 169 + ,null, null], + [null, + 226 + ,null, null], + [null, + 255 + ,null, null], + [null, + 273 + ,null, null], + [null, + 466 + ,null, null], + [null, + 406 + ,null, null], + [null, + 333 + ,null, null], + [null, + 194 + ,null, null], + [null, + 234 + ,null, null], + [null, + 271 + ,null, null], + [null, + 238 + ,null, null], + [null, + 221 + ,null, null], + [null, + 205 + ,null, null], + [null, + 258 + ,null, null], + [null, + 430 + ,null, null], + [null, + 446 + ,null, null], + [null, + 390 + ,null, null], + [null, + 306 + ,null, null], + [null, + 223 + ,null, null], + [null, + 165 + ,null, null], + [null, + 154 + ,null, null], + [null, + 147 + ,null, null], + [null, + 155 + ,null, null], + [null, + 140 + ,null, null], + [null, + 153 + ,null, null], + [null, + 228 + ,null, null], + [null, + 237 + ,null, null], + [null, + 201 + ,null, null], + [null, + 183 + ,null, null], + [null, + 194 + ,null, null], + [null, + 135 + ,null, null], + [null, + 206 + ,null, null], + [null, + 215 + ,null, null], + [null, + 147 + ,null, null], + [null, + 159 + ,null, null], + [null, + 185 + ,null, null], + [null, + 168 + ,null, null], + [null, + 257 + ,null, null], + [null, + 262 + ,null, null], + [null, + 141 + ,null, null], + [null, + 151 + ,null, null], + [null, + 157 + ,null, null], + [null, + 133 + ,null, null], + [null, + 147 + ,null, null], + [null, + 135 + ,null, null], + [null, + 139 + ,null, null], + [null, + 136 + ,null, null], + [null, + 127 + ,null, null], + [null, + 169 + ,null, null], + [null, + 259, null, null + ] + ] + } + ], + "status": "ok", + "time_exec": 0.5533828735351562, + "time_server": 1647935044 +} diff --git a/fixtures/getmeasure_sum_energy_buy_from_grid,sum_energy_buy_from_grid$0,sum_energy_buy_from_grid$1,sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_69.json b/fixtures/getmeasure_sum_energy_buy_from_grid,sum_energy_buy_from_grid$0,sum_energy_buy_from_grid$1,sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_69.json new file mode 100644 index 00000000..3e11402e --- /dev/null +++ b/fixtures/getmeasure_sum_energy_buy_from_grid,sum_energy_buy_from_grid$0,sum_energy_buy_from_grid$1,sum_energy_buy_from_grid$2_98_76_54_32_10_00_00_69.json @@ -0,0 +1,247 @@ +{ + "body": [ + { + "beg_time": 1721772900, + "step_time": 1800, + "value": [ + [ + null, + 20, + null, + null + ], + [ + null, + 19, + null, + null + ], + [ + null, + 16, + null, + null + ], + [ + null, + 11, + null, + null + ], + [ + null, + 18, + null, + null + ], + [ + null, + 14, + null, + null + ], + [ + null, + 14, + null, + null + ], + [ + null, + 64, + null, + null + ], + [ + null, + 100, + null, + null + ], + [ + null, + 58, + null, + null + ], + [ + null, + 39, + null, + null + ], + [ + null, + 24, + null, + null + ], + [ + null, + 19, + null, + null + ], + [ + null, + 19, + null, + null + ], + [ + null, + 712, + null, + null + ], + [ + null, + 724, + null, + null + ], + [ + null, + 711, + null, + null + ], + [ + null, + 498, + 212, + null + ], + [ + null, + null, + 717, + null + ], + [ + null, + null, + 714, + null + ], + [ + null, + null, + 714, + null + ], + [ + null, + null, + 711, + null + ], + [ + null, + null, + 706, + null + ], + [ + null, + null, + 704, + null + ], + [ + null, + null, + 706, + null + ], + [ + null, + null, + 709, + null + ], + [ + null, + null, + 714, + null + ], + [ + null, + null, + 712, + null + ], + [ + null, + null, + 238, + 477 + ], + [ + null, + null, + null, + 714 + ], + [ + null, + null, + null, + 722 + ], + [ + null, + null, + null, + 713 + ], + [ + null, + null, + null, + 709 + ], + [ + null, + null, + null, + 714 + ], + [ + null, + null, + 477, + 241 + ], + [ + null, + null, + 710, + null + ], + [ + null, + null, + 710, + null + ], + [ + null, + null, + 707, + null + ], + [ + null, + null, + 16, + null + ] + ] + } + ], + "status": "ok", + "time_exec": 0.025647878646850586, + "time_server": 1721852913 +} \ No newline at end of file diff --git a/fixtures/homesdata_multi.json b/fixtures/homesdata_multi.json index 7a686ed0..17a53e74 100644 --- a/fixtures/homesdata_multi.json +++ b/fixtures/homesdata_multi.json @@ -1050,12 +1050,11 @@ }, { "id": "98:76:54:32:10:00:00:69", - "type": "NLPO", - "name": "Filtration Piscine", - "setup_date": 1699136266, + "type": "NLPC", + "name": "Compteur Filtration Local Technique", + "setup_date": 1711797163, "room_id": "2754296835", - "bridge": "aa:aa:aa:aa:aa:aa", - "appliance_type": "other" + "bridge": "00:04:74:22:52:38" }, { "id": "98:76:54:32:10:00:00:70", diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index e4a2a158..ac2db08a 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -16,13 +16,12 @@ GETSTATIONDATA_ENDPOINT, HOME, SETSTATE_ENDPOINT, - MeasureInterval, RawData, ) from pyatmo.exceptions import ApiHomeReachabilityError from pyatmo.helpers import extract_raw_data from pyatmo.home import Home -from pyatmo.modules.module import Module +from pyatmo.modules.module import MeasureInterval, Module if TYPE_CHECKING: from pyatmo.auth import AbstractAsyncAuth @@ -73,7 +72,7 @@ def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None: async def async_update_topology( self, disabled_homes_ids: list[str] | None = None - ) -> int: + ) -> None: """Retrieve topology data from /homesdata.""" resp = await self.auth.async_post_api_request( @@ -85,37 +84,21 @@ async def async_update_topology( self.process_topology(disabled_homes_ids=disabled_homes_ids) - return 1 - - async def async_update_status(self, home_id: str | None = None) -> int: - """Retrieve status data from /homestatus. Returns the number of performed API calls.""" - - if home_id is None: - homes = self.homes - else: - homes = [home_id] - num_calls = 0 - all_homes_ok = True - for h_id in homes: - resp = await self.auth.async_post_api_request( - endpoint=GETHOMESTATUS_ENDPOINT, - params={"home_id": h_id}, - ) - raw_data = extract_raw_data(await resp.json(), HOME) - is_correct_update = await self.homes[h_id].update(raw_data) - if not is_correct_update: - all_homes_ok = False - num_calls += 1 - - if all_homes_ok is False: + async def async_update_status(self, home_id: str) -> None: + """Retrieve status data from /homestatus.""" + resp = await self.auth.async_post_api_request( + endpoint=GETHOMESTATUS_ENDPOINT, + params={"home_id": home_id}, + ) + raw_data = extract_raw_data(await resp.json(), HOME) + is_correct_update = await self.homes[home_id].update(raw_data) + if is_correct_update is False: raise ApiHomeReachabilityError( "No Home update could be performed, all modules unreachable and not updated", ) - return num_calls - - async def async_update_events(self, home_id: str) -> int: - """Retrieve events from /getevents. Returns the number of performed API calls.""" + async def async_update_events(self, home_id: str) -> None: + """Retrieve events from /getevents.""" resp = await self.auth.async_post_api_request( endpoint=GETEVENTS_ENDPOINT, params={"home_id": home_id}, @@ -123,23 +106,18 @@ async def async_update_events(self, home_id: str) -> int: raw_data = extract_raw_data(await resp.json(), HOME) await self.homes[home_id].update(raw_data) - return 1 - - async def async_update_weather_stations(self) -> int: - """Retrieve status data from /getstationsdata. Returns the number of performed API calls.""" + async def async_update_weather_stations(self) -> None: + """Retrieve status data from /getstationsdata.""" params = {"get_favorites": ("true" if self.favorite_stations else "false")} await self._async_update_data( GETSTATIONDATA_ENDPOINT, params=params, ) - return 1 - async def async_update_air_care(self) -> int: - """Retrieve status data from /gethomecoachsdata. Returns the number of performed API calls.""" + async def async_update_air_care(self) -> None: + """Retrieve status data from /gethomecoachsdata.""" await self._async_update_data(GETHOMECOACHDATA_ENDPOINT) - return 1 - async def async_update_measures( self, home_id: str, @@ -148,18 +126,15 @@ async def async_update_measures( end_time: int | None = None, interval: MeasureInterval = MeasureInterval.HOUR, days: int = 7, - ) -> int: - """Retrieve measures data from /getmeasure. Returns the number of performed API calls.""" + ) -> None: + """Retrieve measures data from /getmeasure.""" - num_calls = await getattr( - self.homes[home_id].modules[module_id], "async_update_measures" - )( + await getattr(self.homes[home_id].modules[module_id], "async_update_measures")( start_time=start_time, end_time=end_time, interval=interval, days=days, ) - return num_calls def register_public_weather_area( self, @@ -184,8 +159,8 @@ def register_public_weather_area( ) return area_id - async def async_update_public_weather(self, area_id: str) -> int: - """Retrieve status data from /getpublicdata. Returns the number of performed API calls.""" + async def async_update_public_weather(self, area_id: str) -> None: + """Retrieve status data from /getpublicdata.""" params = { "lat_ne": self.public_weather_areas[area_id].location.lat_ne, "lon_ne": self.public_weather_areas[area_id].location.lon_ne, @@ -202,8 +177,6 @@ async def async_update_public_weather(self, area_id: str) -> int: area_id=area_id, ) - return 1 - async def _async_update_data( self, endpoint: str, @@ -216,8 +189,8 @@ async def _async_update_data( raw_data = extract_raw_data(await resp.json(), tag) await self.update_devices(raw_data, area_id) - async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: - """Modify device state by passing JSON specific to the device. Returns the number of performed API calls.""" + async def async_set_state(self, home_id: str, data: dict[str, Any]) -> None: + """Modify device state by passing JSON specific to the device.""" LOG.debug("Setting state: %s", data) post_params = { @@ -233,7 +206,6 @@ async def async_set_state(self, home_id: str, data: dict[str, Any]) -> int: params=post_params, ) LOG.debug("Response: %s", resp) - return 1 async def update_devices( self, diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index b4d3ffe8..e401124d 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -89,42 +89,6 @@ async def async_post_api_request( timeout=timeout, ) - async def async_get_api_request( - self, - endpoint: str, - base_url: str | None = None, - params: dict[str, Any] | None = None, - timeout: int = 5, - ) -> ClientResponse: - """Wrap async post requests.""" - - return await self.async_get_request( - url=(base_url or self.base_url) + endpoint, - params=params, - timeout=timeout, - ) - - async def async_get_request( - self, - url: str, - params: dict[str, Any] | None = None, - timeout: int = 5, - ) -> ClientResponse: - """Wrap async post requests.""" - - access_token = await self.get_access_token() - headers = {AUTHORIZATION_HEADER: f"Bearer {access_token}"} - - req_args = self.prepare_request_get_arguments(params) - - async with self.websession.get( - url, - **req_args, - headers=headers, - timeout=timeout, - ) as resp: - return await self.process_response(resp, url) - async def async_post_request( self, url: str, @@ -167,10 +131,6 @@ def prepare_request_arguments(self, params): return req_args - def prepare_request_get_arguments(self, params): - """Prepare get request arguments.""" - return params - async def process_response(self, resp, url): """Process response.""" resp_status = resp.status @@ -188,8 +148,11 @@ async def handle_error_response(self, resp, resp_status, url): resp_json = await resp.json() message = ( - f"{resp_status} - {ERRORS.get(resp_status, '')} - {resp_json['error']['message']} " - f"({resp_json['error']['code']}) when accessing '{url}'" + f"{resp_status} - " + f"{ERRORS.get(resp_status, '')} - " + f"{resp_json['error']['message']} " + f"({resp_json['error']['code']}) " + f"when accessing '{url}'", ) if resp_status == 403 and resp_json["error"]["code"] == 26: diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index 4485f632..b7132e40 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -2,7 +2,6 @@ from __future__ import annotations -from enum import Enum from typing import Any ERRORS: dict[int, str] = { @@ -46,7 +45,6 @@ GETHOMECOACHDATA_ENDPOINT = "api/gethomecoachsdata" GETMEASURE_ENDPOINT = "api/getmeasure" -GETHOMEMEASURE_ENDPOINT = "api/gethomemeasure" GETSTATIONDATA_ENDPOINT = "api/getstationsdata" GETPUBLIC_DATA_ENDPOINT = "api/getpublicdata" @@ -90,6 +88,7 @@ SCHEDULES = "schedules" EVENTS = "events" + STATION_TEMPERATURE_TYPE = "temperature" STATION_PRESSURE_TYPE = "pressure" STATION_HUMIDITY_TYPE = "humidity" @@ -107,48 +106,4 @@ SCHEDULE_TYPE_THERM = "therm" SCHEDULE_TYPE_EVENT = "event" SCHEDULE_TYPE_ELECTRICITY = "electricity" -SCHEDULE_TYPE_COOLING = "cooling" - -ENERGY_ELEC_PEAK_IDX = 0 -ENERGY_ELEC_OFF_IDX = 1 - - -class MeasureType(Enum): - """Measure type.""" - - BOILERON = "boileron" - BOILEROFF = "boileroff" - SUM_BOILER_ON = "sum_boiler_on" - SUM_BOILER_OFF = "sum_boiler_off" - SUM_ENERGY_ELEC = "sum_energy_buy_from_grid" - SUM_ENERGY_ELEC_BASIC = "sum_energy_buy_from_grid$0" - SUM_ENERGY_ELEC_PEAK = "sum_energy_buy_from_grid$1" - SUM_ENERGY_ELEC_OFF_PEAK = "sum_energy_buy_from_grid$2" - SUM_ENERGY_PRICE = "sum_energy_buy_from_grid_price" - SUM_ENERGY_PRICE_BASIC = "sum_energy_buy_from_grid_price$0" - SUM_ENERGY_PRICE_PEAK = "sum_energy_buy_from_grid_price$1" - SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_buy_from_grid_price$2" - SUM_ENERGY_ELEC_BASIC_OLD = "sum_energy_elec$0" - SUM_ENERGY_ELEC_PEAK_OLD = "sum_energy_elec$1" - SUM_ENERGY_ELEC_OFF_PEAK_OLD = "sum_energy_elec$2" - - -class MeasureInterval(Enum): - """Measure interval.""" - - HALF_HOUR = "30min" - HOUR = "1hour" - THREE_HOURS = "3hours" - DAY = "1day" - WEEK = "1week" - MONTH = "1month" - - -MEASURE_INTERVAL_TO_SECONDS = { - MeasureInterval.HALF_HOUR: 1800, - MeasureInterval.HOUR: 3600, - MeasureInterval.THREE_HOURS: 10800, - MeasureInterval.DAY: 86400, - MeasureInterval.WEEK: 604800, - MeasureInterval.MONTH: 2592000, -} +SCHEDULE_TYPE_COOLING = "cooling" \ No newline at end of file diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 8aa1dd6c..9833d5b0 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -33,6 +33,7 @@ def fix_id(raw_data: RawData) -> dict[str, Any]: def extract_raw_data(resp: Any, tag: str) -> dict[str, Any]: """Extract raw data from server response.""" + raw_data = {} if tag == "body": return {"public": resp["body"], "errors": []} diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 102515f0..7a19e0b3 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -9,11 +9,7 @@ from pyatmo import modules from pyatmo.const import ( - ENERGY_ELEC_OFF_IDX, - ENERGY_ELEC_PEAK_IDX, EVENTS, - SCHEDULE_TYPE_ELECTRICITY, - SCHEDULE_TYPE_THERM, SCHEDULES, SETPERSONSAWAY_ENDPOINT, SETPERSONSHOME_ENDPOINT, @@ -21,7 +17,6 @@ SETTHERMMODE_ENDPOINT, SWITCHHOMESCHEDULE_ENDPOINT, SYNCHOMESCHEDULE_ENDPOINT, - MeasureType, RawData, ) from pyatmo.event import Event @@ -29,7 +24,8 @@ from pyatmo.modules import Module from pyatmo.person import Person from pyatmo.room import Room -from pyatmo.schedule import Schedule, ThermSchedule, schedule_factory +from pyatmo.schedule import Schedule +from pyatmo.modules.module import MeasureType if TYPE_CHECKING: from pyatmo.auth import AbstractAsyncAuth @@ -45,12 +41,12 @@ class Home: name: str rooms: dict[str, Room] modules: dict[str, Module] - schedules: dict[str, Schedule] # for compatibility should diseappear - all_schedules: dict[dict[str, str, Schedule]] | {} + schedules: dict[str, Schedule] persons: dict[str, Person] events: dict[str, Event] - energy_endpoints: list[str] - energy_schedule: list[int] + energy_filters: str + energy_filters_legacy: str + energy_filters_modes: list[str] def __init__(self, auth: AbstractAsyncAuth, raw_data: RawData) -> None: """Initialize a Netatmo home instance.""" @@ -70,86 +66,15 @@ def __init__(self, auth: AbstractAsyncAuth, raw_data: RawData) -> None: ) for room in raw_data.get("rooms", []) } - self._handle_schedules(raw_data.get(SCHEDULES, [])) + self.schedules = { + s["id"]: Schedule(home=self, raw_data=s) + for s in raw_data.get(SCHEDULES, []) + } self.persons = { s["id"]: Person(home=self, raw_data=s) for s in raw_data.get("persons", []) } self.events = {} - def _handle_schedules(self, raw_data): - - schedules = {} - - self.schedules = {} - - for s in raw_data: - # strange but Energy plan are stored in schedules, we should handle this one differently - sched, schedule_type = schedule_factory(home=self, raw_data=s) - if schedule_type not in schedules: - schedules[schedule_type] = {} - schedules[schedule_type][s["id"]] = sched - self.schedules[s["id"]] = sched - - self.all_schedules = schedules - - nrj_schedule = next( - iter(schedules.get(SCHEDULE_TYPE_ELECTRICITY, {}).values()), None - ) - - self.energy_schedule_vals = [] - self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] - self.energy_endpoints_old = [MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value] - if nrj_schedule is not None: - - # Tariff option (basic = always the same price, peak_and_off_peak = peak & off peak hours) - type_tariff = nrj_schedule.tariff_option - zones = nrj_schedule.zones - - if type_tariff == "peak_and_off_peak" and len(zones) >= 2: - - self.energy_endpoints = [None, None] - self.energy_endpoints_old = [None, None] - - self.energy_endpoints[ENERGY_ELEC_PEAK_IDX] = ( - MeasureType.SUM_ENERGY_ELEC_PEAK.value - ) - self.energy_endpoints[ENERGY_ELEC_OFF_IDX] = ( - MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value - ) - - self.energy_endpoints_old[ENERGY_ELEC_PEAK_IDX] = ( - MeasureType.SUM_ENERGY_ELEC_PEAK_OLD.value - ) - self.energy_endpoints_old[ENERGY_ELEC_OFF_IDX] = ( - MeasureType.SUM_ENERGY_ELEC_OFF_PEAK_OLD.value - ) - - if zones[0].price_type == "peak": - peak_id = zones[0].entity_id - else: - peak_id = zones[1].entity_id - - timetable = nrj_schedule.timetable - - # timetable are daily for electricity type, and sorted from begining to end - for t in timetable: - - time = ( - t.m_offset * 60 - ) # m_offset is in minute from the begininng of the day - if len(self.energy_schedule_vals) == 0: - time = 0 - - pos_to_add = ENERGY_ELEC_OFF_IDX - if t.zone_id == peak_id: - pos_to_add = ENERGY_ELEC_PEAK_IDX - - self.energy_schedule_vals.append((time, pos_to_add)) - - else: - self.energy_endpoints = [MeasureType.SUM_ENERGY_ELEC_BASIC.value] - self.energy_endpoints_old = [MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value] - def get_module(self, module: dict) -> Module: """Return module.""" @@ -173,7 +98,10 @@ def update_topology(self, raw_data: RawData) -> None: raw_modules = raw_data.get("modules", []) for module in raw_modules: if (module_id := module["id"]) not in self.modules: - self.modules[module_id] = self.get_module(module) + self.modules[module_id] = getattr(modules, module["type"])( + home=self, + module=module, + ) else: self.modules[module_id].update_topology(module) @@ -196,7 +124,10 @@ def update_topology(self, raw_data: RawData) -> None: for room in self.rooms.keys() - {m["id"] for m in raw_rooms}: self.rooms.pop(room) - self._handle_schedules(raw_data.get(SCHEDULES, [])) + self.schedules = { + s["id"]: Schedule(home=self, raw_data=s) + for s in raw_data.get(SCHEDULES, []) + } async def update(self, raw_data: RawData) -> bool: """Update home with the latest data.""" @@ -249,30 +180,18 @@ async def update(self, raw_data: RawData) -> bool: return True - def get_selected_schedule(self, schedule_type: str = None) -> Schedule | None: + def get_selected_schedule(self) -> Schedule | None: """Return selected schedule for given home.""" - if schedule_type is None: - schedule_type = SCHEDULE_TYPE_THERM - - schedules = self.all_schedules.get(schedule_type, {}) return next( - (schedule for schedule in schedules.values() if schedule.selected), + (schedule for schedule in self.schedules.values() if schedule.selected), None, ) - def get_selected_temperature_schedule(self) -> ThermSchedule | None: - """Return selected temperature schedule for given home.""" - - return self.get_selected_schedule(schedule_type=SCHEDULE_TYPE_THERM) - def is_valid_schedule(self, schedule_id: str) -> bool: """Check if valid schedule.""" - for schedules in self.all_schedules.values(): - if schedule_id in schedules: - return True - return False + return schedule_id in self.schedules def has_otm(self) -> bool: """Check if any room has an OTM device.""" @@ -282,14 +201,14 @@ def has_otm(self) -> bool: def get_hg_temp(self) -> float | None: """Return frost guard temperature value for given home.""" - if (schedule := self.get_selected_temperature_schedule()) is None: + if (schedule := self.get_selected_schedule()) is None: return None return schedule.hg_temp def get_away_temp(self) -> float | None: """Return configured away temperature value for given home.""" - if (schedule := self.get_selected_temperature_schedule()) is None: + if (schedule := self.get_selected_schedule()) is None: return None return schedule.away_temp @@ -381,7 +300,7 @@ async def async_set_schedule_temperatures( ) -> None: """Set the scheduled room temperature for the given schedule ID.""" - selected_schedule = self.get_selected_temperature_schedule() + selected_schedule = self.get_selected_schedule() if selected_schedule is None: raise NoSchedule("Could not determine selected schedule.") diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 7274ca0c..6f188936 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -21,6 +21,7 @@ LOG = logging.getLogger(__name__) + NETATMO_ATTRIBUTES_MAP = { "entity_id": lambda x, y: x.get("id", y), "modules": lambda x, y: x.get("modules_bridged", y), diff --git a/src/pyatmo/modules/device_types.py b/src/pyatmo/modules/device_types.py index e5fbe849..6b2a27a3 100644 --- a/src/pyatmo/modules/device_types.py +++ b/src/pyatmo/modules/device_types.py @@ -7,7 +7,6 @@ LOG = logging.getLogger(__name__) - # pylint: disable=W0613 @@ -206,6 +205,7 @@ class DeviceCategory(str, Enum): DeviceType.NLLF: DeviceCategory.fan, } + DEVICE_DESCRIPTION_MAP: dict[DeviceType, tuple[str, str]] = { # Netatmo Climate/Energy DeviceType.NAPlug: ("Netatmo", "Smart Thermostat Gateway"), diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 683863d8..e90d1ff9 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -24,7 +24,6 @@ LOG = logging.getLogger(__name__) - # pylint: disable=R0901 diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 2578a1d3..b4037f8a 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -2,20 +2,14 @@ from __future__ import annotations -import copy from datetime import datetime, timedelta, timezone +from enum import Enum import logging from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError -from pyatmo.const import ( - ENERGY_ELEC_PEAK_IDX, - GETMEASURE_ENDPOINT, - MEASURE_INTERVAL_TO_SECONDS, - MeasureInterval, - RawData, -) +from pyatmo.const import GETMEASURE_ENDPOINT, RawData from pyatmo.exceptions import ApiError from pyatmo.modules.base_class import EntityBase, NetatmoBase, Place from pyatmo.modules.device_types import DEVICE_CATEGORY_MAP, DeviceCategory, DeviceType @@ -24,7 +18,6 @@ from pyatmo.event import Event from pyatmo.home import Home -import bisect from operator import itemgetter from time import time @@ -471,6 +464,7 @@ def __init__(self, home: Home, module: ModuleT): self.local_url: str | None = None self.is_local: bool | None = None self.alim_status: int | None = None + self.device_type: DeviceType async def async_get_live_snapshot(self) -> bytes | None: """Fetch live camera image.""" @@ -488,7 +482,7 @@ async def async_get_live_snapshot(self) -> bytes | None: async def async_update_camera_urls(self) -> None: """Update and validate the camera urls.""" - if isinstance(self, Module) and self.device_type == "NDB": + if self.device_type == "NDB": self.is_local = None if self.vpn_url and self.is_local: @@ -602,13 +596,53 @@ async def async_monitoring_off(self) -> bool: return await self.async_set_monitoring_state("off") -def _get_proper_in_schedule_index(energy_schedule_vals, srt_beg): - idx = bisect.bisect_left(energy_schedule_vals, srt_beg, key=itemgetter(0)) - if idx >= len(energy_schedule_vals): - idx = len(energy_schedule_vals) - 1 - elif energy_schedule_vals[idx][0] > srt_beg: # if strict equal idx is the good one - idx = max(0, idx - 1) - return idx +class MeasureInterval(Enum): + """Measure interval.""" + + HALF_HOUR = "30min" + HOUR = "1hour" + THREE_HOURS = "3hours" + DAY = "1day" + WEEK = "1week" + MONTH = "1month" + + +class MeasureType(Enum): + """Measure type.""" + + BOILERON = "boileron" + BOILEROFF = "boileroff" + SUM_BOILER_ON = "sum_boiler_on" + SUM_BOILER_OFF = "sum_boiler_off" + SUM_ENERGY_ELEC = "sum_energy_buy_from_grid" + SUM_ENERGY_ELEC_BASIC = "sum_energy_buy_from_grid$0" + SUM_ENERGY_ELEC_PEAK = "sum_energy_buy_from_grid$1" + SUM_ENERGY_ELEC_OFF_PEAK = "sum_energy_buy_from_grid$2" + SUM_ENERGY_PRICE = "sum_energy_buy_from_grid_price" + SUM_ENERGY_PRICE_BASIC = "sum_energy_buy_from_grid_price$0" + SUM_ENERGY_PRICE_PEAK = "sum_energy_buy_from_grid_price$1" + SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_buy_from_grid_price$2" + SUM_ENERGY_ELEC_OLD = "sum_energy_elec" + SUM_ENERGY_ELEC_BASIC_OLD = "sum_energy_elec$0" + SUM_ENERGY_ELEC_PEAK_OLD = "sum_energy_elec$1" + SUM_ENERGY_ELEC_OFF_PEAK_OLD = "sum_energy_elec$2" + + + + + +MEASURE_INTERVAL_TO_SECONDS = { + MeasureInterval.HALF_HOUR: 1800, + MeasureInterval.HOUR: 3600, + MeasureInterval.THREE_HOURS: 10800, + MeasureInterval.DAY: 86400, + MeasureInterval.WEEK: 604800, + MeasureInterval.MONTH: 2592000, +} + +ENERGY_FILTERS = f"{MeasureType.SUM_ENERGY_ELEC.value},{MeasureType.SUM_ENERGY_ELEC_BASIC.value},{MeasureType.SUM_ENERGY_ELEC_PEAK.value},{MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value}" +ENERGY_FILTERS_LEGACY = f"{MeasureType.SUM_ENERGY_ELEC_OLD.value},{MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value},{MeasureType.SUM_ENERGY_ELEC_PEAK_OLD.value},{MeasureType.SUM_ENERGY_ELEC_OFF_PEAK_OLD.value}" +ENERGY_FILTERS_MODES = ["generic", "basic", "peak", "off_peak"] class EnergyHistoryMixin(EntityBase): @@ -712,21 +746,13 @@ def _log_energy_error(self, start_time, end_time, msg=None, body=None): body, ) - def update_measures_num_calls(self): - """Get number of possible endpoint calls.""" - - if not self.home.energy_endpoints: - return 1 - else: - return len(self.home.energy_endpoints) - async def async_update_measures( self, start_time: int | None = None, end_time: int | None = None, interval: MeasureInterval = MeasureInterval.HOUR, days: int = 7, - ) -> int | None: + ) -> None: """Update historical data.""" if end_time is None: @@ -752,25 +778,13 @@ async def async_update_measures( delta_range = MEASURE_INTERVAL_TO_SECONDS.get(interval, 0) // 2 - data_points, num_calls, raw_datas, peak_off_peak_mode = ( - await self._energy_API_calls(start_time, end_time, interval) - ) - - energy_schedule_vals = [] - - if peak_off_peak_mode: - energy_schedule_vals = await self._compute_proper_energy_schedule_offsets( - start_time, end_time, 2 * delta_range, raw_datas, data_points - ) + filters, raw_data = await self._energy_API_calls(start_time, end_time, interval) hist_good_vals = await self._get_aligned_energy_values_and_mode( start_time, end_time, delta_range, - energy_schedule_vals, - peak_off_peak_mode, - raw_datas, - data_points, + raw_data ) self.historical_data = [] @@ -805,11 +819,9 @@ async def async_update_measures( hist_good_vals, prev_end_time, prev_start_time, - prev_sum_energy_elec, - peak_off_peak_mode, + prev_sum_energy_elec ) - return num_calls async def _prepare_exported_historical_data( self, @@ -819,25 +831,26 @@ async def _prepare_exported_historical_data( hist_good_vals, prev_end_time, prev_start_time, - prev_sum_energy_elec, - peak_off_peak_mode, + prev_sum_energy_elec ): computed_start = 0 computed_end = 0 computed_end_for_calculus = 0 - for cur_start_time, val, cur_peak_or_off_peak_mode in hist_good_vals: + for cur_start_time, val, vals in hist_good_vals: self.sum_energy_elec += val - if peak_off_peak_mode: - mode = "off_peak" - if cur_peak_or_off_peak_mode == ENERGY_ELEC_PEAK_IDX: - self.sum_energy_elec_peak += val - mode = "peak" - else: - self.sum_energy_elec_off_peak += val - else: - mode = "standard" + modes = [] + val_modes = [] + + for i, v in enumerate(vals): + if v is not None: + modes.append(ENERGY_FILTERS_MODES[i]) + val_modes.append(v) + if ENERGY_FILTERS_MODES[i] == "off_peak": + self.sum_energy_elec_off_peak += v + elif ENERGY_FILTERS_MODES[i] == "peak": + self.sum_energy_elec_peak += v c_start = cur_start_time c_end = cur_start_time + 2 * delta_range @@ -857,7 +870,8 @@ async def _prepare_exported_historical_data( "startTime": start_time_string, "endTime": end_time_string, "Wh": val, - "energyMode": mode, + "energyMode": modes, + "WhPerModes": val_modes, "startTimeUnix": c_start, "endTimeUnix": c_end, }, @@ -907,203 +921,101 @@ async def _get_aligned_energy_values_and_mode( start_time, end_time, delta_range, - energy_schedule_vals, - peak_off_peak_mode, - raw_datas, - data_points, + raw_data ): hist_good_vals = [] - for cur_peak_off_peak_mode, values_lots in enumerate(raw_datas): - for values_lot in values_lots: - try: - start_lot_time = int(values_lot["beg_time"]) - except Exception: + values_lots = raw_data + + for values_lot in values_lots: + try: + start_lot_time = int(values_lot["beg_time"]) + except Exception: + self._log_energy_error( + start_time, + end_time, + msg=f"beg_time missing", + body=values_lots, + ) + raise ApiError( + f"Energy badly formed resp beg_time missing: {values_lots} - " + f"module: {self.name}" + ) from None + + interval_sec = values_lot.get("step_time") + if interval_sec is None: + if len(values_lot.get("value", [])) > 1: self._log_energy_error( start_time, end_time, - msg=f"beg_time missing {data_points[cur_peak_off_peak_mode]}", + msg=f"step_time missing", body=values_lots, ) - raise ApiError( - f"Energy badly formed resp beg_time missing: {values_lots} - " - f"module: {self.name} - " - f"when accessing '{data_points[cur_peak_off_peak_mode]}'" - ) from None - - interval_sec = values_lot.get("step_time") - if interval_sec is None: - if len(values_lot.get("value", [])) > 1: - self._log_energy_error( - start_time, - end_time, - msg=f"step_time missing {data_points[cur_peak_off_peak_mode]}", - body=values_lots, - ) - interval_sec = 2 * delta_range - else: - interval_sec = int(interval_sec) - - # align the start on the begining of the segment - cur_start_time = start_lot_time - interval_sec // 2 - for val_arr in values_lot.get("value", []): - val = val_arr[0] - - if peak_off_peak_mode: - - d_srt = datetime.fromtimestamp(cur_start_time) - # offset from start of the day - day_origin = int( - datetime(d_srt.year, d_srt.month, d_srt.day).timestamp() - ) - srt_beg = cur_start_time - day_origin - srt_mid = srt_beg + interval_sec // 2 - - # now check if srt_beg is in a schedule span of the right type - idx_limit = _get_proper_in_schedule_index( - energy_schedule_vals, srt_mid - ) - - if ( - self.home.energy_schedule_vals[idx_limit][1] - != cur_peak_off_peak_mode - ): - - # we are NOT in a proper schedule time for this time span ... - # jump to the next one... meaning it is the next day! - if idx_limit == len(energy_schedule_vals) - 1: - # should never append with the performed day extension above - self._log_energy_error( - start_time, - end_time, - msg=f"bad idx missing {data_points[cur_peak_off_peak_mode]}", - body=values_lots, - ) - - raise ApiError( - f"Energy badly formed bad schedule idx in vals: {values_lots} - " - f"module: {self.name} - " - f"when accessing '{data_points[cur_peak_off_peak_mode]}'" - ) - else: - # by construction of the energy schedule the next one should be of opposite mode - if ( - energy_schedule_vals[idx_limit + 1][1] - != cur_peak_off_peak_mode - ): - self._log_energy_error( - start_time, - end_time, - msg=f"bad schedule {data_points[cur_peak_off_peak_mode]}", - body=values_lots, - ) - raise ApiError( - f"Energy badly formed bad schedule: {values_lots} - " - f"module: {self.name} - " - f"when accessing '{data_points[cur_peak_off_peak_mode]}'" - ) - - start_time_to_get_closer = energy_schedule_vals[ - idx_limit + 1 - ][0] - diff_t = start_time_to_get_closer - srt_mid - cur_start_time = ( - day_origin - + srt_beg - + (diff_t // interval_sec + 1) * interval_sec - ) - - hist_good_vals.append( - (cur_start_time, int(val), cur_peak_off_peak_mode) - ) - cur_start_time = cur_start_time + interval_sec + interval_sec = 2 * delta_range + else: + interval_sec = int(interval_sec) + + # align the start on the begining of the segment + cur_start_time = start_lot_time - interval_sec // 2 + for val_arr in values_lot.get("value", []): + vals = [] + val = 0 + for v in val_arr: + if v is not None: + v = int(v) + val += v + vals.append(v) + else: + vals.append(None) + + hist_good_vals.append( + (cur_start_time, val, vals) + ) + cur_start_time = cur_start_time + interval_sec hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) return hist_good_vals - async def _compute_proper_energy_schedule_offsets( - self, start_time, end_time, interval_sec, raw_datas, data_points - ): - max_interval_sec = interval_sec - for cur_peak_or_off_peak_mode, values_lots in enumerate(raw_datas): - for values_lot in values_lots: - local_step_time = values_lot.get("step_time") - - if local_step_time is None: - if len(values_lot.get("value", [])) > 1: - self._log_energy_error( - start_time, - end_time, - msg=f"step_time missing {data_points[cur_peak_or_off_peak_mode]}", - body=values_lots, - ) - else: - local_step_time = int(local_step_time) - max_interval_sec = max(max_interval_sec, local_step_time) - biggest_day_interval = max_interval_sec // (3600 * 24) + 1 - energy_schedule_vals = copy.copy(self.home.energy_schedule_vals) - if energy_schedule_vals[-1][0] < max_interval_sec + (3600 * 24): - if energy_schedule_vals[0][1] == energy_schedule_vals[-1][1]: - # it means the last one continue in the first one the next day - energy_schedule_vals_next = energy_schedule_vals[1:] - else: - energy_schedule_vals_next = copy.copy(self.home.energy_schedule_vals) - - for d in range(0, biggest_day_interval): - next_day_extend = [ - (offset + ((d + 1) * 24 * 3600), mode) - for offset, mode in energy_schedule_vals_next - ] - energy_schedule_vals.extend(next_day_extend) - return energy_schedule_vals - async def _energy_API_calls(self, start_time, end_time, interval): - num_calls = 0 - data_points = self.home.energy_endpoints + + filters = ENERGY_FILTERS # when the bridge is a connected meter, use old endpoints bridge_module = self.home.modules.get(self.bridge) + if bridge_module: if bridge_module.device_type == DeviceType.NLE: - data_points = self.home.energy_endpoints_old + filters = ENERGY_FILTERS_LEGACY + + params = { + "device_id": self.bridge, + "module_id": self.entity_id, + "scale": interval.value, + "type": filters, + "date_begin": start_time, + "date_end": end_time, + } - raw_datas = [] - for data_point in data_points: + resp = await self.home.auth.async_post_api_request( + endpoint=GETMEASURE_ENDPOINT, + params=params, + ) - params = { - "device_id": self.bridge, - "module_id": self.entity_id, - "scale": interval.value, - "type": data_point, - "date_begin": start_time, - "date_end": end_time, - } + rw_dt_f = await resp.json() + rw_dt = rw_dt_f.get("body") - resp = await self.home.auth.async_post_api_request( - endpoint=GETMEASURE_ENDPOINT, - params=params, + if rw_dt is None: + self._log_energy_error( + start_time, end_time, msg=f"direct from {filters}", body=rw_dt_f + ) + raise ApiError( + f"Energy badly formed resp: {rw_dt_f} - " + f"module: {self.name} - " + f"when accessing '{filters}'" ) - rw_dt_f = await resp.json() - rw_dt = rw_dt_f.get("body") - - if rw_dt is None: - self._log_energy_error( - start_time, end_time, msg=f"direct from {data_point}", body=rw_dt_f - ) - raise ApiError( - f"Energy badly formed resp: {rw_dt_f} - " - f"module: {self.name} - " - f"when accessing '{data_point}'" - ) - - num_calls += 1 - raw_datas.append(rw_dt) - - peak_off_peak_mode = False - if len(raw_datas) > 1 and len(self.home.energy_schedule_vals) > 0: - peak_off_peak_mode = True + raw_data = rw_dt - return data_points, num_calls, raw_datas, peak_off_peak_mode + return filters, raw_data class Module(NetatmoBase): @@ -1142,10 +1054,7 @@ async def update(self, raw_data: RawData) -> None: if self.device_type == DeviceType.NLE: # if there is a bridge it means it is a leaf if self.bridge: - bridge_module = self.home.modules.get(self.bridge) - if bridge_module: - if bridge_module.device_type == DeviceType.NLE: - self.reachable = True + self.reachable = True elif self.modules: # this NLE is a bridge itself : make it not available self.reachable = False diff --git a/src/pyatmo/modules/netatmo.py b/src/pyatmo/modules/netatmo.py index d40f9350..047c4eb1 100644 --- a/src/pyatmo/modules/netatmo.py +++ b/src/pyatmo/modules/netatmo.py @@ -43,7 +43,6 @@ LOG = logging.getLogger(__name__) - # pylint: disable=R0901 diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index 98f413fe..d603801e 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -6,13 +6,7 @@ import logging from typing import TYPE_CHECKING -from pyatmo.const import ( - SCHEDULE_TYPE_COOLING, - SCHEDULE_TYPE_ELECTRICITY, - SCHEDULE_TYPE_EVENT, - SCHEDULE_TYPE_THERM, - RawData, -) +from pyatmo.const import RawData from pyatmo.modules.base_class import NetatmoBase from pyatmo.room import Room @@ -27,104 +21,23 @@ class Schedule(NetatmoBase): """Class to represent a Netatmo schedule.""" selected: bool - default: bool - type: str + away_temp: float | None + hg_temp: float | None timetable: list[TimetableEntry] - zones: list[Zone] def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule instance.""" super().__init__(raw_data) self.home = home - self.type = raw_data.get("type", "therm") self.selected = raw_data.get("selected", False) - self.default = raw_data.get("default", False) + self.hg_temp = raw_data.get("hg_temp") + self.away_temp = raw_data.get("away_temp") self.timetable = [ TimetableEntry(home, r) for r in raw_data.get("timetable", []) ] self.zones = [Zone(home, r) for r in raw_data.get("zones", [])] -@dataclass -class ScheduleWithRealZones(Schedule): - """Class to represent a Netatmo schedule.""" - - zones: list[Zone] - - def __init__(self, home: Home, raw_data: RawData) -> None: - """Initialize a Netatmo schedule instance.""" - super().__init__(home, raw_data) - self.zones = [Zone(home, r) for r in raw_data.get("zones", [])] - - -@dataclass -class ThermSchedule(ScheduleWithRealZones): - """Class to represent a Netatmo Temperature schedule.""" - - away_temp: float | None - hg_temp: float | None - - def __init__(self, home: Home, raw_data: RawData) -> None: - """Initialize ThermSchedule.""" - super().__init__(home, raw_data) - self.hg_temp = raw_data.get("hg_temp") - self.away_temp = raw_data.get("away_temp") - - -@dataclass -class CoolingSchedule(ThermSchedule): - """Class to represent a Netatmo Cooling schedule.""" - - cooling_away_temp: float | None - hg_temp: float | None - - def __init__(self, home: Home, raw_data: RawData) -> None: - """Initialize CoolingSchedule.""" - super().__init__(home, raw_data) - self.cooling_away_temp = self.away_temp = raw_data.get( - "cooling_away_temp", self.away_temp - ) - - -@dataclass -class ElectricitySchedule(Schedule): - """Class to represent a Netatmo Energy Plan schedule.""" - - tariff: str - tariff_option: str - power_threshold: int | 6 - contract_power_unit: str # kVA or KW - zones: list[ZoneElectricity] - - def __init__(self, home: Home, raw_data: RawData) -> None: - """Initialize ElectricitySchedule.""" - super().__init__(home, raw_data) - self.tariff = raw_data.get("tariff", "custom") - # Tariff option (basic = always the same price, peak_and_off_peak = peak & offpeak hours) - self.tariff_option = raw_data.get("tariff_option", "basic") - self.power_threshold = raw_data.get("power_threshold", 6) - self.contract_power_unit = raw_data.get("power_threshold", "kVA") - self.zones = [ZoneElectricity(home, r) for r in raw_data.get("zones", [])] - - -@dataclass -class EventSchedule(Schedule): - """Class to represent a Netatmo Energy Plan schedule.""" - - timetable_sunrise: list[TimetableEventEntry] - timetable_sunset: list[TimetableEventEntry] - - def __init__(self, home: Home, raw_data: RawData) -> None: - """Initialize EventSchedule.""" - super().__init__(home, raw_data) - self.timetable_sunrise = [ - TimetableEventEntry(home, r) for r in raw_data.get("timetable_sunrise", []) - ] - self.timetable_sunset = [ - TimetableEventEntry(home, r) for r in raw_data.get("timetable_sunset", []) - ] - - @dataclass class TimetableEntry: """Class to represent a Netatmo schedule's timetable entry.""" @@ -137,41 +50,6 @@ def __init__(self, home: Home, raw_data: RawData) -> None: self.home = home self.zone_id = raw_data.get("zone_id", 0) self.m_offset = raw_data.get("m_offset", 0) - self.twilight_offset = raw_data.get("twilight_offset", 0) - - -@dataclass -class TimetableEventEntry: - """Class to represent a Netatmo schedule's timetable entry.""" - - zone_id: int | None - day: int | 1 - twilight_offset: int | 0 - - def __init__(self, home: Home, raw_data: RawData) -> None: - """Initialize a Netatmo schedule's timetable entry instance.""" - self.home = home - self.zone_id = raw_data.get("zone_id", 0) - self.day = raw_data.get("day", 1) - self.twilight_offset = raw_data.get("twilight_offset", 0) - - -class ModuleSchedule(NetatmoBase): - """Class to represent a Netatmo schedule.""" - - on: bool | None - target_position: int | None - fan_speed: int | None - brightness: int | None - - def __init__(self, home: Home, raw_data: RawData) -> None: - """Initialize a Netatmo schedule's zone instance.""" - super().__init__(raw_data) - self.home = home - self.on = raw_data.get("on", None) - self.target_position = raw_data.get("target_position", None) - self.fan_speed = raw_data.get("fan_speed", None) - self.brightness = raw_data.get("brightness", None) @dataclass @@ -180,7 +58,6 @@ class Zone(NetatmoBase): type: int rooms: list[Room] - modules: list[ModuleSchedule] def __init__(self, home: Home, raw_data: RawData) -> None: """Initialize a Netatmo schedule's zone instance.""" @@ -188,38 +65,9 @@ def __init__(self, home: Home, raw_data: RawData) -> None: self.home = home self.type = raw_data.get("type", 0) - def room_factory(room_home: Home, room_raw_data: RawData): - room = Room(room_home, room_raw_data, {}) + def room_factory(home: Home, room_raw_data: RawData): + room = Room(home, room_raw_data, {}) room.update(room_raw_data) return room self.rooms = [room_factory(home, r) for r in raw_data.get("rooms", [])] - self.modules = [ModuleSchedule(home, m) for m in raw_data.get("modules", [])] - - -@dataclass -class ZoneElectricity(NetatmoBase): - """Class to represent a Netatmo schedule's zone.""" - - price: float - price_type: str - - def __init__(self, home: Home, raw_data: RawData) -> None: - """Initialize a Netatmo schedule's zone instance.""" - super().__init__(raw_data) - self.home = home - self.price = raw_data.get("price", 0.0) - self.price_type = raw_data.get("price_type", "off_peak") - - -def schedule_factory(home: Home, raw_data: RawData) -> (Schedule, str): - """Create proper schedules.""" - - schedule_type = raw_data.get("type", "custom") - cls = { - SCHEDULE_TYPE_THERM: ThermSchedule, - SCHEDULE_TYPE_EVENT: EventSchedule, - SCHEDULE_TYPE_ELECTRICITY: ElectricitySchedule, - SCHEDULE_TYPE_COOLING: CoolingSchedule, - }.get(schedule_type, Schedule) - return cls(home, raw_data), schedule_type diff --git a/tests/test_energy.py b/tests/test_energy.py index 35dd9797..6be08295 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -5,8 +5,7 @@ from unittest.mock import AsyncMock, patch from pyatmo import ApiHomeReachabilityError, DeviceType -from pyatmo.const import MeasureInterval -from pyatmo.modules.module import EnergyHistoryMixin +from pyatmo.modules.module import EnergyHistoryMixin, MeasureInterval import pytest import time_machine @@ -39,13 +38,14 @@ async def test_historical_data_retrieval(async_account): assert module.device_type == DeviceType.NLPC await async_account.async_update_measures(home_id=home_id, module_id=module_id) - # changed teh reference here as start and stop data was not calculated in the spirit of the netatmo api where their time data is in the fact representing the "middle" of the range and not the begining + # changed the reference here as start and stop data was not calculated in the spirit of the netatmo api where their time data is in the fact representing the "middle" of the range and not the begining assert module.historical_data[0] == { "Wh": 197, "duration": 60, "endTime": "2022-02-05T08:59:49Z", "endTimeUnix": 1644051589, - "energyMode": "standard", + "energyMode": ["basic"], + "WhPerModes": [197], "startTime": "2022-02-05T07:59:50Z", "startTimeUnix": 1644047989, } @@ -54,26 +54,30 @@ async def test_historical_data_retrieval(async_account): "duration": 60, "endTime": "2022-02-12T07:59:49Z", "endTimeUnix": 1644652789, - "energyMode": "standard", + "energyMode": ["basic"], + "WhPerModes": [259], "startTime": "2022-02-12T06:59:50Z", "startTimeUnix": 1644649189, } assert len(module.historical_data) == 168 + +@time_machine.travel(dt.datetime(2024, 7, 24, 22, 00, 10)) +@pytest.mark.asyncio async def test_historical_data_retrieval_multi(async_account_multi): """Test retrieval of historical measurements.""" home_id = "aaaaaaaaaaabbbbbbbbbbccc" home = async_account_multi.homes[home_id] - module_id = "98:76:54:32:10:00:00:73" + module_id = "98:76:54:32:10:00:00:69" assert module_id in home.modules module = home.modules[module_id] - assert module.device_type == DeviceType.NLC + assert module.device_type == DeviceType.NLPC - strt = int(dt.datetime.fromisoformat("2024-03-03 00:10:00").timestamp()) - end_time = int(dt.datetime.fromisoformat("2024-03-05 23:59:59").timestamp()) + strt = int(dt.datetime.fromisoformat("2024-07-24 00:00:00").timestamp()) + end_time = int(dt.datetime.fromisoformat("2024-07-24 22:27:00").timestamp()) await async_account_multi.async_update_measures( home_id=home_id, @@ -85,82 +89,41 @@ async def test_historical_data_retrieval_multi(async_account_multi): assert isinstance(module, EnergyHistoryMixin) assert module.historical_data[0] == { - "Wh": 0, + "Wh": 20, "duration": 30, - "endTime": "2024-03-02T23:40:00Z", - "endTimeUnix": 1709422800, - "energyMode": "peak", - "startTime": "2024-03-02T23:10:01Z", - "startTimeUnix": 1709421000, + "endTime": "2024-07-23T22:30:00Z", + "endTimeUnix": 1721773800, + "energyMode": ["basic"], + "WhPerModes": [20], + "startTime": "2024-07-23T22:00:01Z", + "startTimeUnix": 1721772000, } - assert module.historical_data[-1] == { - "Wh": 0, - "duration": 30, - "endTime": "2024-03-05T23:10:00Z", - "endTimeUnix": 1709680200, - "energyMode": "peak", - "startTime": "2024-03-05T22:40:01Z", - "startTimeUnix": 1709678400, + assert module.historical_data[17] == { + 'Wh': 710, + 'WhPerModes': [498, 212], + 'duration': 30, + 'endTime': '2024-07-24T07:00:00Z', + 'endTimeUnix': 1721804400, + 'energyMode': ['basic', 'peak'], + 'startTime': '2024-07-24T06:30:01Z', + 'startTimeUnix': 1721802600 } - assert len(module.historical_data) == 134 - - assert ( - module.sum_energy_elec - == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak - ) - assert module.sum_energy_elec_off_peak == 11219 - assert module.sum_energy_elec_peak == 31282 - - -async def test_historical_data_retrieval_multi_2(async_account_multi): - """Test retrieval of historical measurements.""" - home_id = "aaaaaaaaaaabbbbbbbbbbccc" - - home = async_account_multi.homes[home_id] - - module_id = "98:76:54:32:10:00:00:49" - assert module_id in home.modules - module = home.modules[module_id] - assert module.device_type == DeviceType.NLC - - strt = int(dt.datetime.fromisoformat("2024-03-15 00:29:51").timestamp()) - end = int(dt.datetime.fromisoformat("2024-03-15 13:45:24").timestamp()) - await async_account_multi.async_update_measures( - home_id=home_id, - module_id=module_id, - interval=MeasureInterval.HALF_HOUR, - start_time=strt, - end_time=end, - ) - - assert module.historical_data[0] == { - "Wh": 0, - "duration": 30, - "endTime": "2024-03-14T23:59:51Z", - "endTimeUnix": 1710460791, - "energyMode": "peak", - "startTime": "2024-03-14T23:29:52Z", - "startTimeUnix": 1710458991, - } assert module.historical_data[-1] == { - "Wh": 0, - "duration": 30, - "endTime": "2024-03-15T12:59:51Z", - "endTimeUnix": 1710507591, - "energyMode": "peak", - "startTime": "2024-03-15T12:29:52Z", - "startTimeUnix": 1710505791, + 'Wh': 16, + 'WhPerModes': [16], + 'duration': 30, + 'endTime': '2024-07-24T17:30:00Z', + 'endTimeUnix': 1721842200, + 'energyMode': ['peak'], + 'startTime': '2024-07-24T17:00:01Z', + 'startTimeUnix': 1721840400 } - assert len(module.historical_data) == 26 - - assert ( - module.sum_energy_elec - == module.sum_energy_elec_peak + module.sum_energy_elec_off_peak - ) - assert module.sum_energy_elec_off_peak == 780 - assert module.sum_energy_elec_peak == 890 + assert len(module.historical_data) == 39 + assert module.sum_energy_elec == 17547 + assert module.sum_energy_elec_off_peak == 4290 + assert module.sum_energy_elec_peak == 10177 async def test_disconnected_main_bridge(async_account_multi): """Test retrieval of historical measurements.""" diff --git a/tests/testing_main_template.py b/tests/testing_main_template.py index e3fe3183..bbf03304 100644 --- a/tests/testing_main_template.py +++ b/tests/testing_main_template.py @@ -4,8 +4,7 @@ from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError import asyncio -from pyatmo.const import MeasureInterval - +from pyatmo.modules.module import MeasureInterval MY_TOKEN_FROM_NETATMO = "MY_TOKEN" From 2bfdb12832f6e352c2d613b4e678a255608f51fd Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 24 Jul 2024 23:40:46 +0200 Subject: [PATCH 80/97] Remove schedule code API calls, etc ... to make it compatible with old lib --- src/pyatmo/modules/module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index b4037f8a..5053c4d0 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -662,11 +662,11 @@ def __init__(self, home: Home, module: ModuleT): self._last_energy_from_API_end_for_power_adjustment_calculus: int | None = None self.in_reset: bool | False = False - def reset_measures(self): + def reset_measures(self, start_power_time, in_reset=True): """Reset energy measures.""" - self.in_reset = True + self.in_reset = in_reset self.historical_data = [] - self._last_energy_from_API_end_for_power_adjustment_calculus = None + self._last_energy_from_API_end_for_power_adjustment_calculus = start_power_time self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 From d791f753051524b23105b7ca5ccd47b2b23cb12d Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 24 Jul 2024 23:50:52 +0200 Subject: [PATCH 81/97] Remove schedule code API calls, etc ... to make it compatible with old lib --- src/pyatmo/modules/module.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 5053c4d0..f2b91d39 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -666,7 +666,10 @@ def reset_measures(self, start_power_time, in_reset=True): """Reset energy measures.""" self.in_reset = in_reset self.historical_data = [] - self._last_energy_from_API_end_for_power_adjustment_calculus = start_power_time + if start_power_time is None: + self._last_energy_from_API_end_for_power_adjustment_calculus = start_power_time + else: + self._last_energy_from_API_end_for_power_adjustment_calculus = int(start_power_time.timestamp()) self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 @@ -712,7 +715,7 @@ def get_sum_energy_elec_power_adapted( if self.in_reset is False: if to_ts is None: - to_ts = time() + to_ts = int(time()) from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus From 6ec554ed827316e06635c40efde3ca996b0bc8a6 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 25 Jul 2024 00:17:27 +0200 Subject: [PATCH 82/97] Remove schedule code API calls, etc ... to make it compatible with old lib --- src/pyatmo/home.py | 5 +---- src/pyatmo/modules/legrand.py | 3 ++- src/pyatmo/modules/module.py | 16 ++++++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 7a19e0b3..64a0518f 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -98,10 +98,7 @@ def update_topology(self, raw_data: RawData) -> None: raw_modules = raw_data.get("modules", []) for module in raw_modules: if (module_id := module["id"]) not in self.modules: - self.modules[module_id] = getattr(modules, module["type"])( - home=self, - module=module, - ) + self.modules[module_id] = self.get_module(module) else: self.modules[module_id].update_topology(module) diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index e90d1ff9..2584711d 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -20,6 +20,7 @@ Switch, SwitchMixin, WifiMixin, + EnergyHistoryLegacyMixin, ) LOG = logging.getLogger(__name__) @@ -99,7 +100,7 @@ class NLPC(FirmwareMixin, EnergyHistoryMixin, PowerMixin, Module): """Legrand / BTicino connected energy meter.""" -class NLE(FirmwareMixin, EnergyHistoryMixin, Module): +class NLE(FirmwareMixin, EnergyHistoryLegacyMixin, Module): """Legrand / BTicino connected ecometer. no power supported for the NLE (in the home status API).""" diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index f2b91d39..97f042e1 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -978,16 +978,12 @@ async def _get_aligned_energy_values_and_mode( hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) return hist_good_vals - async def _energy_API_calls(self, start_time, end_time, interval): - - filters = ENERGY_FILTERS + def get_energy_filers(self): + return ENERGY_FILTERS - # when the bridge is a connected meter, use old endpoints - bridge_module = self.home.modules.get(self.bridge) + async def _energy_API_calls(self, start_time, end_time, interval): - if bridge_module: - if bridge_module.device_type == DeviceType.NLE: - filters = ENERGY_FILTERS_LEGACY + filters = self.get_energy_filers() params = { "device_id": self.bridge, @@ -1020,6 +1016,10 @@ async def _energy_API_calls(self, start_time, end_time, interval): return filters, raw_data +class EnergyHistoryLegacyMixin(EnergyHistoryMixin): + def get_energy_filers(self): + return ENERGY_FILTERS_LEGACY + class Module(NetatmoBase): """Class to represent a Netatmo module.""" From aa1240ff4876c2f33f7559e53a4b12b5df5076e5 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 25 Jul 2024 01:03:55 +0200 Subject: [PATCH 83/97] Black / Rust --- src/pyatmo/const.py | 5 ---- src/pyatmo/home.py | 1 - src/pyatmo/modules/module.py | 51 +++++++++++++--------------------- tests/test_energy.py | 32 ++++++++++----------- tests/testing_main_template.py | 3 +- 5 files changed, 36 insertions(+), 56 deletions(-) diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index b7132e40..f182dfc2 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -102,8 +102,3 @@ ACCESSORY_WIND_TIME_TYPE = "wind_timeutc" ACCESSORY_GUST_STRENGTH_TYPE = "gust_strength" ACCESSORY_GUST_ANGLE_TYPE = "gust_angle" - -SCHEDULE_TYPE_THERM = "therm" -SCHEDULE_TYPE_EVENT = "event" -SCHEDULE_TYPE_ELECTRICITY = "electricity" -SCHEDULE_TYPE_COOLING = "cooling" \ No newline at end of file diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 64a0518f..a1eaa02d 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -25,7 +25,6 @@ from pyatmo.person import Person from pyatmo.room import Room from pyatmo.schedule import Schedule -from pyatmo.modules.module import MeasureType if TYPE_CHECKING: from pyatmo.auth import AbstractAsyncAuth diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 97f042e1..74bc2298 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -627,10 +627,6 @@ class MeasureType(Enum): SUM_ENERGY_ELEC_PEAK_OLD = "sum_energy_elec$1" SUM_ENERGY_ELEC_OFF_PEAK_OLD = "sum_energy_elec$2" - - - - MEASURE_INTERVAL_TO_SECONDS = { MeasureInterval.HALF_HOUR: 1800, MeasureInterval.HOUR: 3600, @@ -646,7 +642,7 @@ class MeasureType(Enum): class EnergyHistoryMixin(EntityBase): - """Mixin for history data.""" + """Mixin for Energy history data.""" def __init__(self, home: Home, module: ModuleT): """Initialize history mixin.""" @@ -659,7 +655,7 @@ def __init__(self, home: Home, module: ModuleT): self.sum_energy_elec: int | None = None self.sum_energy_elec_peak: int | None = None self.sum_energy_elec_off_peak: int | None = None - self._last_energy_from_API_end_for_power_adjustment_calculus: int | None = None + self._achor_for_power_adjustment: int | None = None self.in_reset: bool | False = False def reset_measures(self, start_power_time, in_reset=True): @@ -667,9 +663,9 @@ def reset_measures(self, start_power_time, in_reset=True): self.in_reset = in_reset self.historical_data = [] if start_power_time is None: - self._last_energy_from_API_end_for_power_adjustment_calculus = start_power_time + self._achor_for_power_adjustment = start_power_time else: - self._last_energy_from_API_end_for_power_adjustment_calculus = int(start_power_time.timestamp()) + self._achor_for_power_adjustment = int(start_power_time.timestamp()) self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 @@ -717,7 +713,7 @@ def get_sum_energy_elec_power_adapted( if to_ts is None: to_ts = int(time()) - from_ts = self._last_energy_from_API_end_for_power_adjustment_calculus + from_ts = self._achor_for_power_adjustment if ( from_ts is not None @@ -784,10 +780,7 @@ async def async_update_measures( filters, raw_data = await self._energy_API_calls(start_time, end_time, interval) hist_good_vals = await self._get_aligned_energy_values_and_mode( - start_time, - end_time, - delta_range, - raw_data + start_time, end_time, delta_range, raw_data ) self.historical_data = [] @@ -797,7 +790,7 @@ async def async_update_measures( self.sum_energy_elec_off_peak = 0 # no data at all: we know nothing for the end: best guess, it is the start - self._last_energy_from_API_end_for_power_adjustment_calculus = start_time + self._achor_for_power_adjustment = start_time self.in_reset = False @@ -822,7 +815,7 @@ async def async_update_measures( hist_good_vals, prev_end_time, prev_start_time, - prev_sum_energy_elec + prev_sum_energy_elec, ) @@ -834,7 +827,7 @@ async def _prepare_exported_historical_data( hist_good_vals, prev_end_time, prev_start_time, - prev_sum_energy_elec + prev_sum_energy_elec, ): computed_start = 0 computed_end = 0 @@ -915,17 +908,11 @@ async def _prepare_exported_historical_data( self.sum_energy_elec, prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING", ) - self._last_energy_from_API_end_for_power_adjustment_calculus = ( + self._achor_for_power_adjustment = ( computed_end_for_calculus ) - async def _get_aligned_energy_values_and_mode( - self, - start_time, - end_time, - delta_range, - raw_data - ): + async def _get_aligned_energy_values_and_mode(self, start_time, end_time, delta_range, raw_data): hist_good_vals = [] values_lots = raw_data @@ -936,7 +923,7 @@ async def _get_aligned_energy_values_and_mode( self._log_energy_error( start_time, end_time, - msg=f"beg_time missing", + msg="beg_time missing", body=values_lots, ) raise ApiError( @@ -950,7 +937,7 @@ async def _get_aligned_energy_values_and_mode( self._log_energy_error( start_time, end_time, - msg=f"step_time missing", + msg="step_time missing", body=values_lots, ) interval_sec = 2 * delta_range @@ -970,20 +957,18 @@ async def _get_aligned_energy_values_and_mode( else: vals.append(None) - hist_good_vals.append( - (cur_start_time, val, vals) - ) + hist_good_vals.append((cur_start_time, val, vals)) cur_start_time = cur_start_time + interval_sec hist_good_vals = sorted(hist_good_vals, key=itemgetter(0)) return hist_good_vals - def get_energy_filers(self): + def _get_energy_filers(self): return ENERGY_FILTERS async def _energy_API_calls(self, start_time, end_time, interval): - filters = self.get_energy_filers() + filters = self._get_energy_filers() params = { "device_id": self.bridge, @@ -1016,8 +1001,10 @@ async def _energy_API_calls(self, start_time, end_time, interval): return filters, raw_data + class EnergyHistoryLegacyMixin(EnergyHistoryMixin): - def get_energy_filers(self): + """Mixin for Energy history data. Using legacy APis (used for NLE)""" + def _get_energy_filers(self): return ENERGY_FILTERS_LEGACY diff --git a/tests/test_energy.py b/tests/test_energy.py index 6be08295..05e49cc2 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -99,25 +99,25 @@ async def test_historical_data_retrieval_multi(async_account_multi): "startTimeUnix": 1721772000, } assert module.historical_data[17] == { - 'Wh': 710, - 'WhPerModes': [498, 212], - 'duration': 30, - 'endTime': '2024-07-24T07:00:00Z', - 'endTimeUnix': 1721804400, - 'energyMode': ['basic', 'peak'], - 'startTime': '2024-07-24T06:30:01Z', - 'startTimeUnix': 1721802600 + "Wh": 710, + "WhPerModes": [498, 212], + "duration": 30, + "endTime": "2024-07-24T07:00:00Z", + "endTimeUnix": 1721804400, + "energyMode": ["basic", "peak"], + "startTime": "2024-07-24T06:30:01Z", + "startTimeUnix": 1721802600 } assert module.historical_data[-1] == { - 'Wh': 16, - 'WhPerModes': [16], - 'duration': 30, - 'endTime': '2024-07-24T17:30:00Z', - 'endTimeUnix': 1721842200, - 'energyMode': ['peak'], - 'startTime': '2024-07-24T17:00:01Z', - 'startTimeUnix': 1721840400 + "Wh": 16, + "WhPerModes": [16], + "duration": 30, + "endTime": "2024-07-24T17:30:00Z", + "endTimeUnix": 1721842200, + "energyMode": ["peak"], + "startTime": "2024-07-24T17:00:01Z", + "startTimeUnix": 1721840400 } assert len(module.historical_data) == 39 diff --git a/tests/testing_main_template.py b/tests/testing_main_template.py index bbf03304..df09d84b 100644 --- a/tests/testing_main_template.py +++ b/tests/testing_main_template.py @@ -1,7 +1,6 @@ import pyatmo -import aiohttp from pyatmo.auth import AbstractAsyncAuth -from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError +from aiohttp import ClientSession import asyncio from pyatmo.modules.module import MeasureInterval From 9be36e2628f3525dc10a13bf8440bbddf17e5340 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 25 Jul 2024 01:13:14 +0200 Subject: [PATCH 84/97] Black / Rust --- src/pyatmo/modules/module.py | 16 ++++++++++------ tests/test_energy.py | 5 +++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 74bc2298..7babd4ed 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -627,6 +627,7 @@ class MeasureType(Enum): SUM_ENERGY_ELEC_PEAK_OLD = "sum_energy_elec$1" SUM_ENERGY_ELEC_OFF_PEAK_OLD = "sum_energy_elec$2" + MEASURE_INTERVAL_TO_SECONDS = { MeasureInterval.HALF_HOUR: 1800, MeasureInterval.HOUR: 3600, @@ -818,7 +819,6 @@ async def async_update_measures( prev_sum_energy_elec, ) - async def _prepare_exported_historical_data( self, start_time, @@ -908,11 +908,13 @@ async def _prepare_exported_historical_data( self.sum_energy_elec, prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING", ) - self._achor_for_power_adjustment = ( - computed_end_for_calculus - ) - async def _get_aligned_energy_values_and_mode(self, start_time, end_time, delta_range, raw_data): + + self._achor_for_power_adjustment = computed_end_for_calculus + + async def _get_aligned_energy_values_and_mode( + self, start_time, end_time, delta_range, raw_data + ): hist_good_vals = [] values_lots = raw_data @@ -1002,8 +1004,10 @@ async def _energy_API_calls(self, start_time, end_time, interval): return filters, raw_data + class EnergyHistoryLegacyMixin(EnergyHistoryMixin): - """Mixin for Energy history data. Using legacy APis (used for NLE)""" + """Mixin for Energy history data, Using legacy APis (used for NLE).""" + def _get_energy_filers(self): return ENERGY_FILTERS_LEGACY diff --git a/tests/test_energy.py b/tests/test_energy.py index 05e49cc2..abf36e35 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -106,7 +106,7 @@ async def test_historical_data_retrieval_multi(async_account_multi): "endTimeUnix": 1721804400, "energyMode": ["basic", "peak"], "startTime": "2024-07-24T06:30:01Z", - "startTimeUnix": 1721802600 + "startTimeUnix": 1721802600, } assert module.historical_data[-1] == { @@ -117,7 +117,7 @@ async def test_historical_data_retrieval_multi(async_account_multi): "endTimeUnix": 1721842200, "energyMode": ["peak"], "startTime": "2024-07-24T17:00:01Z", - "startTimeUnix": 1721840400 + "startTimeUnix": 1721840400, } assert len(module.historical_data) == 39 @@ -125,6 +125,7 @@ async def test_historical_data_retrieval_multi(async_account_multi): assert module.sum_energy_elec_off_peak == 4290 assert module.sum_energy_elec_peak == 10177 + async def test_disconnected_main_bridge(async_account_multi): """Test retrieval of historical measurements.""" home_id = "aaaaaaaaaaabbbbbbbbbbccc" From ea5dd4e362ae1b463f4435b7116220a567e1cf0f Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 25 Jul 2024 01:18:08 +0200 Subject: [PATCH 85/97] Black / Rust --- src/pyatmo/modules/legrand.py | 2 +- src/pyatmo/modules/module.py | 4 +--- tests/test_energy.py | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 2584711d..24b54da8 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -10,6 +10,7 @@ DimmableMixin, Dimmer, EnergyHistoryMixin, + EnergyHistoryLegacyMixin, Fan, FirmwareMixin, Module, @@ -20,7 +21,6 @@ Switch, SwitchMixin, WifiMixin, - EnergyHistoryLegacyMixin, ) LOG = logging.getLogger(__name__) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 7babd4ed..3daaefd4 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -909,11 +909,10 @@ async def _prepare_exported_historical_data( prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING", ) - self._achor_for_power_adjustment = computed_end_for_calculus async def _get_aligned_energy_values_and_mode( - self, start_time, end_time, delta_range, raw_data + self, start_time, end_time, delta_range, raw_data ): hist_good_vals = [] values_lots = raw_data @@ -1004,7 +1003,6 @@ async def _energy_API_calls(self, start_time, end_time, interval): return filters, raw_data - class EnergyHistoryLegacyMixin(EnergyHistoryMixin): """Mixin for Energy history data, Using legacy APis (used for NLE).""" diff --git a/tests/test_energy.py b/tests/test_energy.py index abf36e35..511bf815 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -62,7 +62,6 @@ async def test_historical_data_retrieval(async_account): assert len(module.historical_data) == 168 - @time_machine.travel(dt.datetime(2024, 7, 24, 22, 00, 10)) @pytest.mark.asyncio async def test_historical_data_retrieval_multi(async_account_multi): From c8bca7defe18a83106b2d8a217ccdd9a66eb0143 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 25 Jul 2024 01:19:35 +0200 Subject: [PATCH 86/97] Black / Rust --- src/pyatmo/modules/legrand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 24b54da8..058a5df8 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -9,8 +9,8 @@ ContactorMixin, DimmableMixin, Dimmer, - EnergyHistoryMixin, EnergyHistoryLegacyMixin, + EnergyHistoryMixin, Fan, FirmwareMixin, Module, From 4d204c95f006b3f75eb89803e7e0f856e3ff8581 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 25 Jul 2024 01:35:29 +0200 Subject: [PATCH 87/97] Black / Rust --- tests/test_home.py | 1 - tests/testing_main_template.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_home.py b/tests/test_home.py index f73723b0..70e85b6c 100644 --- a/tests/test_home.py +++ b/tests/test_home.py @@ -8,7 +8,6 @@ from pyatmo import DeviceType, NoDevice import pytest - from tests.common import MockResponse # pylint: disable=F6401 diff --git a/tests/testing_main_template.py b/tests/testing_main_template.py index df09d84b..5d17423a 100644 --- a/tests/testing_main_template.py +++ b/tests/testing_main_template.py @@ -1,8 +1,8 @@ -import pyatmo -from pyatmo.auth import AbstractAsyncAuth -from aiohttp import ClientSession import asyncio +from aiohttp import ClientSession +import pyatmo +from pyatmo.auth import AbstractAsyncAuth from pyatmo.modules.module import MeasureInterval MY_TOKEN_FROM_NETATMO = "MY_TOKEN" @@ -38,11 +38,11 @@ async def main(): end_time=end, ) - print(account) + # print(account) if __name__ == "__main__": topology = asyncio.run(main()) - print(topology) + # print(topology) From d80c56218cfa701d14d535a88be7882bdec9a24c Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 25 Jul 2024 01:44:32 +0200 Subject: [PATCH 88/97] Black / Ruff --- src/pyatmo/home.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index a1eaa02d..ce4adb84 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -43,9 +43,6 @@ class Home: schedules: dict[str, Schedule] persons: dict[str, Person] events: dict[str, Event] - energy_filters: str - energy_filters_legacy: str - energy_filters_modes: list[str] def __init__(self, auth: AbstractAsyncAuth, raw_data: RawData) -> None: """Initialize a Netatmo home instance.""" From ad7c58e006805c0fc8cd67ec3d3877e90c7e1bc2 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 25 Jul 2024 02:33:47 +0200 Subject: [PATCH 89/97] Black / Ruff --- src/pyatmo/modules/module.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 3daaefd4..cd3db434 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -622,10 +622,10 @@ class MeasureType(Enum): SUM_ENERGY_PRICE_BASIC = "sum_energy_buy_from_grid_price$0" SUM_ENERGY_PRICE_PEAK = "sum_energy_buy_from_grid_price$1" SUM_ENERGY_PRICE_OFF_PEAK = "sum_energy_buy_from_grid_price$2" - SUM_ENERGY_ELEC_OLD = "sum_energy_elec" - SUM_ENERGY_ELEC_BASIC_OLD = "sum_energy_elec$0" - SUM_ENERGY_ELEC_PEAK_OLD = "sum_energy_elec$1" - SUM_ENERGY_ELEC_OFF_PEAK_OLD = "sum_energy_elec$2" + SUM_ENERGY_ELEC_LEGACY = "sum_energy_elec" + SUM_ENERGY_ELEC_BASIC_LEGACY = "sum_energy_elec$0" + SUM_ENERGY_ELEC_PEAK_LEGACY = "sum_energy_elec$1" + SUM_ENERGY_ELEC_OFF_PEAK_LEGACY = "sum_energy_elec$2" MEASURE_INTERVAL_TO_SECONDS = { @@ -638,7 +638,7 @@ class MeasureType(Enum): } ENERGY_FILTERS = f"{MeasureType.SUM_ENERGY_ELEC.value},{MeasureType.SUM_ENERGY_ELEC_BASIC.value},{MeasureType.SUM_ENERGY_ELEC_PEAK.value},{MeasureType.SUM_ENERGY_ELEC_OFF_PEAK.value}" -ENERGY_FILTERS_LEGACY = f"{MeasureType.SUM_ENERGY_ELEC_OLD.value},{MeasureType.SUM_ENERGY_ELEC_BASIC_OLD.value},{MeasureType.SUM_ENERGY_ELEC_PEAK_OLD.value},{MeasureType.SUM_ENERGY_ELEC_OFF_PEAK_OLD.value}" +ENERGY_FILTERS_LEGACY = f"{MeasureType.SUM_ENERGY_ELEC_LEGACY.value},{MeasureType.SUM_ENERGY_ELEC_BASIC_LEGACY.value},{MeasureType.SUM_ENERGY_ELEC_PEAK_LEGACY.value},{MeasureType.SUM_ENERGY_ELEC_OFF_PEAK_LEGACY.value}" ENERGY_FILTERS_MODES = ["generic", "basic", "peak", "off_peak"] @@ -656,7 +656,7 @@ def __init__(self, home: Home, module: ModuleT): self.sum_energy_elec: int | None = None self.sum_energy_elec_peak: int | None = None self.sum_energy_elec_off_peak: int | None = None - self._achor_for_power_adjustment: int | None = None + self._anchor_for_power_adjustment: int | None = None self.in_reset: bool | False = False def reset_measures(self, start_power_time, in_reset=True): @@ -664,14 +664,14 @@ def reset_measures(self, start_power_time, in_reset=True): self.in_reset = in_reset self.historical_data = [] if start_power_time is None: - self._achor_for_power_adjustment = start_power_time + self._anchor_for_power_adjustment = start_power_time else: - self._achor_for_power_adjustment = int(start_power_time.timestamp()) + self._anchor_for_power_adjustment = int(start_power_time.timestamp()) self.sum_energy_elec = 0 self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - def compute_rieman_sum(self, power_data, conservative: bool = False): + def compute_riemann_sum(self, power_data, conservative: bool = False): """Compute energy from power with a rieman sum.""" delta_energy = 0 @@ -709,12 +709,12 @@ def get_sum_energy_elec_power_adapted( delta_energy = 0 - if self.in_reset is False: + if not self.in_reset: if to_ts is None: to_ts = int(time()) - from_ts = self._achor_for_power_adjustment + from_ts = self._anchor_for_power_adjustment if ( from_ts is not None @@ -728,7 +728,7 @@ def get_sum_energy_elec_power_adapted( if isinstance( self, EnergyHistoryMixin ): # well to please the linter.... - delta_energy = self.compute_rieman_sum(power_data, conservative) + delta_energy = self.compute_riemann_sum(power_data, conservative) return v, delta_energy @@ -791,7 +791,7 @@ async def async_update_measures( self.sum_energy_elec_off_peak = 0 # no data at all: we know nothing for the end: best guess, it is the start - self._achor_for_power_adjustment = start_time + self._anchor_for_power_adjustment = start_time self.in_reset = False @@ -909,7 +909,7 @@ async def _prepare_exported_historical_data( prev_sum_energy_elec if prev_sum_energy_elec is not None else "NOTHING", ) - self._achor_for_power_adjustment = computed_end_for_calculus + self._anchor_for_power_adjustment = computed_end_for_calculus async def _get_aligned_energy_values_and_mode( self, start_time, end_time, delta_range, raw_data From 0673532ee97f8ae6d57ea8c64aec4a5b8627c92c Mon Sep 17 00:00:00 2001 From: tmenguy Date: Fri, 16 Aug 2024 18:55:55 +0200 Subject: [PATCH 90/97] some first remarks from PR fixes --- src/pyatmo/account.py | 9 ++++-- src/pyatmo/modules/base_class.py | 47 ++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index ac2db08a..606d611c 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -37,7 +37,7 @@ def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True) -> N self.auth: AbstractAsyncAuth = auth self.user: str | None = None - self.all_account_homes_id: dict[str, str] = {} + self.all_homes_id: dict[str, str] = {} self.homes: dict[str, Home] = {} self.raw_data: RawData = {} self.favorite_stations: bool = favorite_stations @@ -54,13 +54,16 @@ def __repr__(self) -> str: def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None: """Process topology information from /homesdata.""" + if disabled_homes_ids is None: + disabled_homes_ids = [] + for home in self.raw_data["homes"]: home_id = home.get("id", "Unknown") home_name = home.get("name", "Unknown") - self.all_account_homes_id[home_id] = home_name + self.all_homes_id[home_id] = home_name - if disabled_homes_ids and home_id in disabled_homes_ids: + if home_id in disabled_homes_ids: if home_id in self.homes: del self.homes[home_id] continue diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 6f188936..17d33c66 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -61,7 +61,7 @@ class EntityBase: # 2 days of dynamic historical data stored -MAX_HISTORY_TIME_S = 24 * 2 * 3600 +MAX_HISTORY_TIME_FRAME = 24 * 2 * 3600 class NetatmoBase(EntityBase, ABC): @@ -98,29 +98,34 @@ def _update_attributes(self, raw_data: RawData) -> None: now = int(time()) for hist_feature in self.history_features: if hist_feature in self.__dict__: - hist_f = self.history_features_values.get(hist_feature, None) - if hist_f is None: - hist_f = [] - self.history_features_values[hist_feature] = hist_f val = getattr(self, hist_feature) if val is None: continue - if not hist_f or hist_f[-1][0] <= now: - hist_f.append((now, val, self.entity_id)) - else: - i = bisect.bisect_left(hist_f, now, key=itemgetter(0)) - - if i < len(hist_f): - if hist_f[i][0] == now: - hist_f[i] = (now, val, self.entity_id) - i = None - - if i is not None: - hist_f.insert(i, (now, val, self.entity_id)) - - # keep timing history to a maximum representative time - while len(hist_f) > 0 and now - hist_f[0][0] > MAX_HISTORY_TIME_S: - hist_f.pop(0) + + self.add_history_data(hist_feature, val, now) + + + def add_history_data(self, feature: str, value, time: int) -> None: + """Add historical data at the given time.""" + + # get the feature values rolling buffer + hist_f = self.history_features_values.setdefault(feature, []) + if not hist_f or hist_f[-1][0] <= time: + hist_f.append((time, value, self.entity_id)) + else: + i = bisect.bisect_left(hist_f, time, key=itemgetter(0)) + + if i < len(hist_f): + if hist_f[i][0] == time: + hist_f[i] = (time, value, self.entity_id) + i = None + + if i is not None: + hist_f.insert(i, (time, value, self.entity_id)) + + # keep timing history to a maximum representative time + while len(hist_f) > 0 and hist_f[-1][0] - hist_f[0][0] > MAX_HISTORY_TIME_FRAME: + hist_f.pop(0) def get_history_data(self, feature: str, from_ts: int, to_ts: int | None = None): """Retrieve historical data.""" From ae6b4fc950e13d71dc43aa19c5e59612b57c3cee Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sat, 17 Aug 2024 00:28:52 +0200 Subject: [PATCH 91/97] removed home update boolean as output, and raise conditionally a reachability error --- src/pyatmo/account.py | 6 +----- src/pyatmo/home.py | 12 +++++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 606d611c..61023e6f 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -94,11 +94,7 @@ async def async_update_status(self, home_id: str) -> None: params={"home_id": home_id}, ) raw_data = extract_raw_data(await resp.json(), HOME) - is_correct_update = await self.homes[home_id].update(raw_data) - if is_correct_update is False: - raise ApiHomeReachabilityError( - "No Home update could be performed, all modules unreachable and not updated", - ) + await self.homes[home_id].update(raw_data, do_raise_for_reachability_error=True) async def async_update_events(self, home_id: str) -> None: """Retrieve events from /getevents.""" diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index ce4adb84..78e96bc1 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -20,7 +20,7 @@ RawData, ) from pyatmo.event import Event -from pyatmo.exceptions import InvalidSchedule, InvalidState, NoSchedule +from pyatmo.exceptions import InvalidSchedule, InvalidState, NoSchedule, ApiHomeReachabilityError from pyatmo.modules import Module from pyatmo.person import Person from pyatmo.room import Room @@ -122,7 +122,7 @@ def update_topology(self, raw_data: RawData) -> None: for s in raw_data.get(SCHEDULES, []) } - async def update(self, raw_data: RawData) -> bool: + async def update(self, raw_data: RawData, do_raise_for_reachability_error=False) -> None: """Update home with the latest data.""" num_errors = 0 for module in raw_data.get("errors", []): @@ -164,14 +164,16 @@ async def update(self, raw_data: RawData) -> bool: ], ) - if ( + if (do_raise_for_reachability_error and num_errors > 0 and has_one_module_reachable is False and has_an_update is False ): - return False + raise ApiHomeReachabilityError( + "No Home update could be performed, all modules unreachable and not updated", + ) + - return True def get_selected_schedule(self) -> Schedule | None: """Return selected schedule for given home.""" From 06bc34cb9afb0fbb9c504ed8e52a52a246fa49d8 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sat, 17 Aug 2024 00:36:15 +0200 Subject: [PATCH 92/97] removed home update boolean as output, and raise conditionally a reachability error --- src/pyatmo/account.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 61023e6f..466337cc 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -18,7 +18,6 @@ SETSTATE_ENDPOINT, RawData, ) -from pyatmo.exceptions import ApiHomeReachabilityError from pyatmo.helpers import extract_raw_data from pyatmo.home import Home from pyatmo.modules.module import MeasureInterval, Module From 542f9b92b988986ca5a932d79111eac0dd26defa Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sat, 17 Aug 2024 01:22:39 +0200 Subject: [PATCH 93/97] rieman energy method extraction --- src/pyatmo/modules/module.py | 57 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index cd3db434..453e13d0 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -642,6 +642,34 @@ class MeasureType(Enum): ENERGY_FILTERS_MODES = ["generic", "basic", "peak", "off_peak"] +def compute_riemann_sum(power_data : list[tuple[int, float]], conservative: bool = False): + """Compute energy from power with a rieman sum.""" + + delta_energy = 0 + if power_data and len(power_data) > 1: + + # compute a rieman sum, as best as possible , trapezoidal, taking pessimistic asumption + # as we don't want to artifically go up the previous one + # (except in rare exceptions like reset, 0 , etc) + + for i in range(len(power_data) - 1): + + dt_h = float(power_data[i + 1][0] - power_data[i][0]) / 3600.0 + + if conservative: + d_p_w = 0 + else: + d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) + + d_nrj_wh = dt_h * ( + min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w + ) + + delta_energy += d_nrj_wh + + return delta_energy + + class EnergyHistoryMixin(EntityBase): """Mixin for Energy history data.""" @@ -671,33 +699,6 @@ def reset_measures(self, start_power_time, in_reset=True): self.sum_energy_elec_peak = 0 self.sum_energy_elec_off_peak = 0 - def compute_riemann_sum(self, power_data, conservative: bool = False): - """Compute energy from power with a rieman sum.""" - - delta_energy = 0 - if power_data and len(power_data) > 1: - - # compute a rieman sum, as best as possible , trapezoidal, taking pessimistic asumption - # as we don't want to artifically go up the previous one - # (except in rare exceptions like reset, 0 , etc) - - for i in range(len(power_data) - 1): - - dt_h = float(power_data[i + 1][0] - power_data[i][0]) / 3600.0 - - if conservative: - d_p_w = 0 - else: - d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) - - d_nrj_wh = dt_h * ( - min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w - ) - - delta_energy += d_nrj_wh - - return delta_energy - def get_sum_energy_elec_power_adapted( self, to_ts: int | float | None = None, conservative: bool = False ): @@ -728,7 +729,7 @@ def get_sum_energy_elec_power_adapted( if isinstance( self, EnergyHistoryMixin ): # well to please the linter.... - delta_energy = self.compute_riemann_sum(power_data, conservative) + delta_energy = compute_riemann_sum(power_data, conservative) return v, delta_energy From c60813469d830da4faf5c45e7eeec7d57d1c4ec1 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 18 Aug 2024 00:33:21 +0200 Subject: [PATCH 94/97] last PR comments --- src/pyatmo/const.py | 3 +++ src/pyatmo/home.py | 8 ++++---- src/pyatmo/modules/base_class.py | 6 +----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index f182dfc2..1a471d67 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -102,3 +102,6 @@ ACCESSORY_WIND_TIME_TYPE = "wind_timeutc" ACCESSORY_GUST_STRENGTH_TYPE = "gust_strength" ACCESSORY_GUST_ANGLE_TYPE = "gust_angle" + +# 2 days of dynamic historical data stored +MAX_HISTORY_TIME_FRAME = 24 * 2 * 3600 diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 78e96bc1..b5b6edf5 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -124,9 +124,9 @@ def update_topology(self, raw_data: RawData) -> None: async def update(self, raw_data: RawData, do_raise_for_reachability_error=False) -> None: """Update home with the latest data.""" - num_errors = 0 + has_error = False for module in raw_data.get("errors", []): - num_errors += 1 + has_error = True await self.modules[module["id"]].update({}) data = raw_data["home"] @@ -164,8 +164,8 @@ async def update(self, raw_data: RawData, do_raise_for_reachability_error=False) ], ) - if (do_raise_for_reachability_error and - num_errors > 0 + if (do_raise_for_reachability_error + and has_error and has_one_module_reachable is False and has_an_update is False ): diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 17d33c66..3f05af77 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -10,7 +10,7 @@ from operator import itemgetter from typing import TYPE_CHECKING, Any -from pyatmo.const import RawData +from pyatmo.const import RawData, MAX_HISTORY_TIME_FRAME from pyatmo.modules.device_types import DeviceType if TYPE_CHECKING: @@ -60,10 +60,6 @@ class EntityBase: name: str | None -# 2 days of dynamic historical data stored -MAX_HISTORY_TIME_FRAME = 24 * 2 * 3600 - - class NetatmoBase(EntityBase, ABC): """Base class for Netatmo entities.""" From ba590c66668e0f6f25b3a3a6d433882c4c31d8a8 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sun, 18 Aug 2024 23:30:24 +0200 Subject: [PATCH 95/97] black / ruff --- src/pyatmo/home.py | 16 +++++++++++----- src/pyatmo/modules/base_class.py | 3 +-- src/pyatmo/modules/module.py | 6 ++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index b5b6edf5..71b5fbed 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -20,7 +20,12 @@ RawData, ) from pyatmo.event import Event -from pyatmo.exceptions import InvalidSchedule, InvalidState, NoSchedule, ApiHomeReachabilityError +from pyatmo.exceptions import ( + ApiHomeReachabilityError, + InvalidSchedule, + InvalidState, + NoSchedule, +) from pyatmo.modules import Module from pyatmo.person import Person from pyatmo.room import Room @@ -122,7 +127,9 @@ def update_topology(self, raw_data: RawData) -> None: for s in raw_data.get(SCHEDULES, []) } - async def update(self, raw_data: RawData, do_raise_for_reachability_error=False) -> None: + async def update( + self, raw_data: RawData, do_raise_for_reachability_error=False + ) -> None: """Update home with the latest data.""" has_error = False for module in raw_data.get("errors", []): @@ -164,7 +171,8 @@ async def update(self, raw_data: RawData, do_raise_for_reachability_error=False) ], ) - if (do_raise_for_reachability_error + if ( + do_raise_for_reachability_error and has_error and has_one_module_reachable is False and has_an_update is False @@ -173,8 +181,6 @@ async def update(self, raw_data: RawData, do_raise_for_reachability_error=False) "No Home update could be performed, all modules unreachable and not updated", ) - - def get_selected_schedule(self) -> Schedule | None: """Return selected schedule for given home.""" diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 3f05af77..5541a23c 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -10,7 +10,7 @@ from operator import itemgetter from typing import TYPE_CHECKING, Any -from pyatmo.const import RawData, MAX_HISTORY_TIME_FRAME +from pyatmo.const import MAX_HISTORY_TIME_FRAME, RawData from pyatmo.modules.device_types import DeviceType if TYPE_CHECKING: @@ -100,7 +100,6 @@ def _update_attributes(self, raw_data: RawData) -> None: self.add_history_data(hist_feature, val, now) - def add_history_data(self, feature: str, value, time: int) -> None: """Add historical data at the given time.""" diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index 453e13d0..64901659 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -642,7 +642,9 @@ class MeasureType(Enum): ENERGY_FILTERS_MODES = ["generic", "basic", "peak", "off_peak"] -def compute_riemann_sum(power_data : list[tuple[int, float]], conservative: bool = False): +def compute_riemann_sum( + power_data: list[tuple[int, float]], conservative: bool = False +): """Compute energy from power with a rieman sum.""" delta_energy = 0 @@ -662,7 +664,7 @@ def compute_riemann_sum(power_data : list[tuple[int, float]], conservative: bool d_p_w = abs(float(power_data[i + 1][1] - power_data[i][1])) d_nrj_wh = dt_h * ( - min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w + min(power_data[i + 1][1], power_data[i][1]) + 0.5 * d_p_w ) delta_energy += d_nrj_wh From 2da7edc56d1b7860a7108319e120bf7312d9d555 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Wed, 11 Sep 2024 00:38:55 +0200 Subject: [PATCH 96/97] better error handling --- src/pyatmo/home.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 81531d1a..aed14ade 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -150,6 +150,7 @@ async def update( self.rooms[room["id"]].update(room) for person_status in data.get("persons", []): + has_an_update = True if person := self.persons.get(person_status["id"]): person.update(person_status) From e42f54333f0c789b1e75ab3bd95af5abb22351ce Mon Sep 17 00:00:00 2001 From: tmenguy Date: Thu, 26 Sep 2024 20:59:17 +0200 Subject: [PATCH 97/97] add a comment --- src/pyatmo/home.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 4b197bd6..9a7542ff 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -179,6 +179,7 @@ async def update( self.rooms[room["id"]].update(room) for person_status in data.get("persons", []): + # if there is a person update, it means the house has been updated has_an_update = True if person := self.persons.get(person_status["id"]): person.update(person_status)