From 0e1d12b1ae23801ed4396aae096c43a551b023bc Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 2 Oct 2025 17:26:49 +0200 Subject: [PATCH 01/33] Fix Z-Wave RGB light turn on causing rare `ZeroDivisionError` (#153422) --- homeassistant/components/zwave_js/light.py | 17 ++-- tests/components/zwave_js/test_light.py | 96 ++++++++++++++++++++++ 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 9b7c0222410033..a5d54cf80c1119 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -612,10 +612,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: # If brightness gets set, preserve the color and mix it with the new brightness if self.color_mode == ColorMode.HS: scale = brightness / 255 - if ( - self._last_on_color is not None - and None not in self._last_on_color.values() - ): + if self._last_on_color is not None: # Changed brightness from 0 to >0 old_brightness = max(self._last_on_color.values()) new_scale = brightness / old_brightness @@ -634,8 +631,9 @@ async def async_turn_on(self, **kwargs: Any) -> None: elif current_brightness is not None: scale = current_brightness / 255 - # Reset last color until turning off again + # Reset last color and brightness until turning off again self._last_on_color = None + self._last_brightness = None if new_colors is None: new_colors = self._get_new_colors( @@ -651,8 +649,10 @@ async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - # Remember last color and brightness to restore it when turning on - self._last_brightness = self.brightness + # Remember last color and brightness to restore it when turning on, + # only if we're sure the light is turned on to avoid overwriting good values + if self._last_brightness is None: + self._last_brightness = self.brightness if self._current_color and isinstance(self._current_color.value, dict): red = self._current_color.value.get(COLOR_SWITCH_COMBINED_RED) green = self._current_color.value.get(COLOR_SWITCH_COMBINED_GREEN) @@ -666,7 +666,8 @@ async def async_turn_off(self, **kwargs: Any) -> None: if blue is not None: last_color[ColorComponent.BLUE] = blue - if last_color: + # Only store the last color if we're aware of it, i.e. ignore off light + if last_color and max(last_color.values()) > 0: self._last_on_color = last_color if self._target_brightness: diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 954d6422399487..f58f8427cf2b2d 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -1073,6 +1073,16 @@ async def update_color(red: int, green: int, blue: int) -> None: ) await update_color(0, 0, 0) + # Turn off again and make sure last color/brightness is still preserved + # when turning on light again in the next step + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, + blocking=True, + ) + await update_color(0, 0, 0) + client.async_send_command.reset_mock() # Assert that the brightness is preserved when turning on with color @@ -1095,6 +1105,92 @@ async def update_color(red: int, green: int, blue: int) -> None: client.async_send_command.reset_mock() + await update_color(0, 0, 123) + + # Turn off twice + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, + blocking=True, + ) + await update_color(0, 0, 0) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, + blocking=True, + ) + await update_color(0, 0, 0) + + state = hass.states.get(HSM200_V1_ENTITY) + assert state.state == STATE_OFF + + client.async_send_command.reset_mock() + + # Assert that turning on after successive off calls works and keeps the last color + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_BRIGHTNESS: 150}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + } + assert args["value"] == {"red": 0, "green": 0, "blue": 150} + + client.async_send_command.reset_mock() + + await update_color(0, 0, 150) + + # Force the light to turn off + await update_color(0, 0, 0) + + # Turn off already off light, we won't be aware of last color and brightness + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, + blocking=True, + ) + await update_color(0, 0, 0) + + state = hass.states.get(HSM200_V1_ENTITY) + assert state.state == STATE_OFF + + client.async_send_command.reset_mock() + + # Assert that turning on light after off call with unknown off color/brightness state + # works and that light turns on to white with specified brightness + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY, ATTR_BRIGHTNESS: 160}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + } + assert args["value"] == {"red": 160, "green": 160, "blue": 160} + + client.async_send_command.reset_mock() + + await update_color(160, 160, 160) + # Clear the color value to trigger an unknown state event = Event( type="value updated", From 7ab99c028c8c7ea8fae86d9b0f64fc88aaaeb936 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:29:14 +0200 Subject: [PATCH 02/33] Add new test fixture for Tuya wk category (#153457) --- tests/components/tuya/__init__.py | 1 + .../components/tuya/fixtures/wk_tfbhw0mg.json | 109 ++++++++++++++++++ .../tuya/snapshots/test_climate.ambr | 77 +++++++++++++ .../components/tuya/snapshots/test_init.ambr | 31 +++++ .../tuya/snapshots/test_sensor.ambr | 53 +++++++++ .../tuya/snapshots/test_switch.ambr | 48 ++++++++ 6 files changed, 319 insertions(+) create mode 100644 tests/components/tuya/fixtures/wk_tfbhw0mg.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 8659f277ad5c40..13c24046d2f0f4 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -236,6 +236,7 @@ "wk_fi6dne5tu4t1nm6j", # https://github.com/orgs/home-assistant/discussions/243 "wk_gc1bxoq2hafxpa35", # https://github.com/home-assistant/core/issues/145551 "wk_gogb05wrtredz3bs", # https://github.com/home-assistant/core/issues/136337 + "wk_tfbhw0mg", # https://github.com/home-assistant/core/issues/152282 "wk_y5obtqhuztqsf2mj", # https://github.com/home-assistant/core/issues/139735 "wkcz_gc4b1mdw7kebtuyz", # https://github.com/home-assistant/core/issues/135617 "wkf_9xfjixap", # https://github.com/home-assistant/core/issues/139966 diff --git a/tests/components/tuya/fixtures/wk_tfbhw0mg.json b/tests/components/tuya/fixtures/wk_tfbhw0mg.json new file mode 100644 index 00000000000000..4a9186314eaf1d --- /dev/null +++ b/tests/components/tuya/fixtures/wk_tfbhw0mg.json @@ -0,0 +1,109 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Salon", + "category": "wk", + "product_id": "tfbhw0mg", + "product_name": "ZX-5442", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-09-13T13:48:55+00:00", + "create_time": "2025-09-13T13:48:55+00:00", + "update_time": "2025-09-13T13:48:55+00:00", + "function": { + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u00b0C", + "min": 1, + "max": 59, + "scale": 1, + "step": 5 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["auto", "manual", "holiday"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "holiday_set": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u00b0C", + "min": 1, + "max": 59, + "scale": 1, + "step": 5 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u00b0C", + "min": -50, + "max": 350, + "scale": 1, + "step": 5 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["auto", "manual", "holiday"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["fault1", "fault2", "fault3"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 1000, + "scale": 2, + "step": 1 + } + }, + "holiday_set": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status": { + "temp_set": 59, + "temp_current": 203, + "mode": "manual", + "child_lock": false, + "fault": 0, + "battery_percentage": 117, + "holiday_set": "FAwZAAAiAAA=" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index 2673383f4f2f53..87304e5e9adbd6 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -860,6 +860,83 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[climate.salon-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 5.9, + 'min_temp': 0.1, + 'preset_modes': list([ + 'holiday', + ]), + 'target_temp_step': 0.5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.salon', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.gm0whbftkw', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.salon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.3, + 'friendly_name': 'Salon', + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 5.9, + 'min_temp': 0.1, + 'preset_mode': None, + 'preset_modes': list([ + 'holiday', + ]), + 'supported_features': , + 'target_temp_step': 0.5, + 'temperature': 5.9, + }), + 'context': , + 'entity_id': 'climate.salon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- # name: test_platform_setup_and_discovery[climate.smart_thermostats-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index c8810beb0e230d..67ca9ddec1a174 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -3657,6 +3657,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[gm0whbftkw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'gm0whbftkw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ZX-5442', + 'model_id': 'tfbhw0mg', + 'name': 'Salon', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[gnZOKztbAtcBkEGPzc] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 442f6774a0aa7d..6d20cc5c03da7d 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -15293,6 +15293,59 @@ 'state': 'high', }) # --- +# name: test_platform_setup_and_discovery[sensor.salon_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.salon_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.gm0whbftkwbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.salon_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Salon Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.salon_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.17', + }) +# --- # name: test_platform_setup_and_discovery[sensor.sapphire_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 1b30c6cabea44e..041f0eda4f566f 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -7016,6 +7016,54 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[switch.salon_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.salon_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.gm0whbftkwchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.salon_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Salon Child lock', + }), + 'context': , + 'entity_id': 'switch.salon_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[switch.sapphire_socket-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From ee4a1de5660f930f37119f7d7225838b628bb7c7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 2 Oct 2025 17:45:37 +0200 Subject: [PATCH 03/33] Add translation for turbo fan mode in SmartThings (#153445) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/smartthings/icons.json | 11 +++++++++++ homeassistant/components/smartthings/strings.json | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index c7c531785b5160..aad9182576d413 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -31,6 +31,17 @@ "default": "mdi:stop" } }, + "climate": { + "air_conditioner": { + "state_attributes": { + "fan_mode": { + "state": { + "turbo": "mdi:wind-power" + } + } + } + } + }, "number": { "washer_rinse_cycles": { "default": "mdi:waves-arrow-up" diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 244324bb1b4c02..fb6b84651865e6 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -89,6 +89,11 @@ "long_wind": "Long wind", "smart": "Smart" } + }, + "fan_mode": { + "state": { + "turbo": "Turbo" + } } } } From a172f67d37659efa2e96201522deffe34a98c2b0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 2 Oct 2025 18:26:33 +0200 Subject: [PATCH 04/33] Fix Nord Pool 15 minute interval (#153350) --- homeassistant/components/nordpool/__init__.py | 1 + .../components/nordpool/coordinator.py | 41 +- tests/components/nordpool/conftest.py | 8 +- .../nordpool/fixtures/delivery_period_nl.json | 684 +++++- .../fixtures/delivery_period_today.json | 824 ++++++- .../fixtures/delivery_period_tomorrow.json | 826 +++++-- .../fixtures/delivery_period_yesterday.json | 250 +-- .../nordpool/fixtures/indices_15.json | 584 ++--- .../nordpool/fixtures/indices_60.json | 152 +- .../nordpool/snapshots/test_diagnostics.ambr | 1988 +++++++++++++---- .../nordpool/snapshots/test_sensor.ambr | 104 +- .../nordpool/snapshots/test_services.ambr | 1224 ++++++---- tests/components/nordpool/test_config_flow.py | 10 +- tests/components/nordpool/test_coordinator.py | 43 +- tests/components/nordpool/test_diagnostics.py | 2 +- tests/components/nordpool/test_init.py | 10 +- tests/components/nordpool/test_sensor.py | 54 +- tests/components/nordpool/test_services.py | 22 +- 18 files changed, 5019 insertions(+), 1808 deletions(-) diff --git a/homeassistant/components/nordpool/__init__.py b/homeassistant/components/nordpool/__init__.py index dd2626aaa41c71..8fb6a5eaf3b4a1 100644 --- a/homeassistant/components/nordpool/__init__.py +++ b/homeassistant/components/nordpool/__init__.py @@ -34,6 +34,7 @@ async def async_setup_entry( coordinator = NordPoolDataUpdateCoordinator(hass, config_entry) await coordinator.fetch_data(dt_util.utcnow(), True) + await coordinator.update_listeners(dt_util.utcnow()) if not coordinator.last_update_success: raise ConfigEntryNotReady( translation_domain=DOMAIN, diff --git a/homeassistant/components/nordpool/coordinator.py b/homeassistant/components/nordpool/coordinator.py index 51bc0e638dd82f..f2f41322aff225 100644 --- a/homeassistant/components/nordpool/coordinator.py +++ b/homeassistant/components/nordpool/coordinator.py @@ -44,9 +44,10 @@ def __init__(self, hass: HomeAssistant, config_entry: NordPoolConfigEntry) -> No name=DOMAIN, ) self.client = NordPoolClient(session=async_get_clientsession(hass)) - self.unsub: Callable[[], None] | None = None + self.data_unsub: Callable[[], None] | None = None + self.listener_unsub: Callable[[], None] | None = None - def get_next_interval(self, now: datetime) -> datetime: + def get_next_data_interval(self, now: datetime) -> datetime: """Compute next time an update should occur.""" next_hour = dt_util.utcnow() + timedelta(hours=1) next_run = datetime( @@ -56,23 +57,45 @@ def get_next_interval(self, now: datetime) -> datetime: next_hour.hour, tzinfo=dt_util.UTC, ) - LOGGER.debug("Next update at %s", next_run) + LOGGER.debug("Next data update at %s", next_run) + return next_run + + def get_next_15_interval(self, now: datetime) -> datetime: + """Compute next time we need to notify listeners.""" + next_run = dt_util.utcnow() + timedelta(minutes=15) + next_minute = next_run.minute // 15 * 15 + next_run = next_run.replace( + minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC + ) + + LOGGER.debug("Next listener update at %s", next_run) return next_run async def async_shutdown(self) -> None: """Cancel any scheduled call, and ignore new runs.""" await super().async_shutdown() - if self.unsub: - self.unsub() - self.unsub = None + if self.data_unsub: + self.data_unsub() + self.data_unsub = None + if self.listener_unsub: + self.listener_unsub() + self.listener_unsub = None + + async def update_listeners(self, now: datetime) -> None: + """Update entity listeners.""" + self.listener_unsub = async_track_point_in_utc_time( + self.hass, + self.update_listeners, + self.get_next_15_interval(dt_util.utcnow()), + ) + self.async_update_listeners() async def fetch_data(self, now: datetime, initial: bool = False) -> None: """Fetch data from Nord Pool.""" - self.unsub = async_track_point_in_utc_time( - self.hass, self.fetch_data, self.get_next_interval(dt_util.utcnow()) + self.data_unsub = async_track_point_in_utc_time( + self.hass, self.fetch_data, self.get_next_data_interval(dt_util.utcnow()) ) if self.config_entry.pref_disable_polling and not initial: - self.async_update_listeners() return try: data = await self.handle_data(initial) diff --git a/tests/components/nordpool/conftest.py b/tests/components/nordpool/conftest.py index ca1e2a05a0b488..2f5318d515c702 100644 --- a/tests/components/nordpool/conftest.py +++ b/tests/components/nordpool/conftest.py @@ -47,7 +47,7 @@ async def get_data_from_library( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-05", + "date": "2025-10-01", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", @@ -58,7 +58,7 @@ async def get_data_from_library( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-05", + "date": "2025-10-01", "market": "DayAhead", "deliveryArea": "SE3", "currency": "EUR", @@ -69,7 +69,7 @@ async def get_data_from_library( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-04", + "date": "2025-09-30", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", @@ -80,7 +80,7 @@ async def get_data_from_library( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-06", + "date": "2025-10-02", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", diff --git a/tests/components/nordpool/fixtures/delivery_period_nl.json b/tests/components/nordpool/fixtures/delivery_period_nl.json index cd326e05d01c42..2c99e5614a2973 100644 --- a/tests/components/nordpool/fixtures/delivery_period_nl.json +++ b/tests/components/nordpool/fixtures/delivery_period_nl.json @@ -1,213 +1,717 @@ { - "deliveryDateCET": "2024-11-05", + "deliveryDateCET": "2025-10-01", "version": 2, - "updatedAt": "2024-11-04T11:58:10.7711584Z", + "updatedAt": "2025-09-30T11:08:13.1885499Z", "deliveryAreas": ["NL"], "market": "DayAhead", "multiAreaEntries": [ { - "deliveryStart": "2024-11-04T23:00:00Z", - "deliveryEnd": "2024-11-05T00:00:00Z", + "deliveryStart": "2025-09-30T22:00:00Z", + "deliveryEnd": "2025-09-30T22:15:00Z", "entryPerArea": { - "NL": 83.63 + "NL": 102.55 } }, { - "deliveryStart": "2024-11-05T00:00:00Z", - "deliveryEnd": "2024-11-05T01:00:00Z", + "deliveryStart": "2025-09-30T22:15:00Z", + "deliveryEnd": "2025-09-30T22:30:00Z", "entryPerArea": { - "NL": 94.0 + "NL": 92.17 } }, { - "deliveryStart": "2024-11-05T01:00:00Z", - "deliveryEnd": "2024-11-05T02:00:00Z", + "deliveryStart": "2025-09-30T22:30:00Z", + "deliveryEnd": "2025-09-30T22:45:00Z", "entryPerArea": { - "NL": 90.68 + "NL": 82.69 } }, { - "deliveryStart": "2024-11-05T02:00:00Z", - "deliveryEnd": "2024-11-05T03:00:00Z", + "deliveryStart": "2025-09-30T22:45:00Z", + "deliveryEnd": "2025-09-30T23:00:00Z", "entryPerArea": { - "NL": 91.3 + "NL": 81.86 } }, { - "deliveryStart": "2024-11-05T03:00:00Z", - "deliveryEnd": "2024-11-05T04:00:00Z", + "deliveryStart": "2025-09-30T23:00:00Z", + "deliveryEnd": "2025-09-30T23:15:00Z", "entryPerArea": { - "NL": 94.0 + "NL": 89.54 } }, { - "deliveryStart": "2024-11-05T04:00:00Z", - "deliveryEnd": "2024-11-05T05:00:00Z", + "deliveryStart": "2025-09-30T23:15:00Z", + "deliveryEnd": "2025-09-30T23:30:00Z", "entryPerArea": { - "NL": 96.09 + "NL": 84.93 } }, { - "deliveryStart": "2024-11-05T05:00:00Z", - "deliveryEnd": "2024-11-05T06:00:00Z", + "deliveryStart": "2025-09-30T23:30:00Z", + "deliveryEnd": "2025-09-30T23:45:00Z", "entryPerArea": { - "NL": 106.0 + "NL": 83.56 } }, { - "deliveryStart": "2024-11-05T06:00:00Z", - "deliveryEnd": "2024-11-05T07:00:00Z", + "deliveryStart": "2025-09-30T23:45:00Z", + "deliveryEnd": "2025-10-01T00:00:00Z", "entryPerArea": { - "NL": 135.99 + "NL": 81.69 } }, { - "deliveryStart": "2024-11-05T07:00:00Z", - "deliveryEnd": "2024-11-05T08:00:00Z", + "deliveryStart": "2025-10-01T00:00:00Z", + "deliveryEnd": "2025-10-01T00:15:00Z", "entryPerArea": { - "NL": 136.21 + "NL": 81.87 } }, { - "deliveryStart": "2024-11-05T08:00:00Z", - "deliveryEnd": "2024-11-05T09:00:00Z", + "deliveryStart": "2025-10-01T00:15:00Z", + "deliveryEnd": "2025-10-01T00:30:00Z", "entryPerArea": { - "NL": 118.23 + "NL": 81.51 } }, { - "deliveryStart": "2024-11-05T09:00:00Z", - "deliveryEnd": "2024-11-05T10:00:00Z", + "deliveryStart": "2025-10-01T00:30:00Z", + "deliveryEnd": "2025-10-01T00:45:00Z", "entryPerArea": { - "NL": 105.87 + "NL": 77.42 } }, { - "deliveryStart": "2024-11-05T10:00:00Z", - "deliveryEnd": "2024-11-05T11:00:00Z", + "deliveryStart": "2025-10-01T00:45:00Z", + "deliveryEnd": "2025-10-01T01:00:00Z", "entryPerArea": { - "NL": 95.28 + "NL": 76.45 } }, { - "deliveryStart": "2024-11-05T11:00:00Z", - "deliveryEnd": "2024-11-05T12:00:00Z", + "deliveryStart": "2025-10-01T01:00:00Z", + "deliveryEnd": "2025-10-01T01:15:00Z", "entryPerArea": { - "NL": 94.92 + "NL": 79.32 } }, { - "deliveryStart": "2024-11-05T12:00:00Z", - "deliveryEnd": "2024-11-05T13:00:00Z", + "deliveryStart": "2025-10-01T01:15:00Z", + "deliveryEnd": "2025-10-01T01:30:00Z", "entryPerArea": { - "NL": 99.25 + "NL": 79.24 } }, { - "deliveryStart": "2024-11-05T13:00:00Z", - "deliveryEnd": "2024-11-05T14:00:00Z", + "deliveryStart": "2025-10-01T01:30:00Z", + "deliveryEnd": "2025-10-01T01:45:00Z", "entryPerArea": { - "NL": 107.98 + "NL": 80.05 } }, { - "deliveryStart": "2024-11-05T14:00:00Z", - "deliveryEnd": "2024-11-05T15:00:00Z", + "deliveryStart": "2025-10-01T01:45:00Z", + "deliveryEnd": "2025-10-01T02:00:00Z", "entryPerArea": { - "NL": 149.86 + "NL": 79.52 } }, { - "deliveryStart": "2024-11-05T15:00:00Z", - "deliveryEnd": "2024-11-05T16:00:00Z", + "deliveryStart": "2025-10-01T02:00:00Z", + "deliveryEnd": "2025-10-01T02:15:00Z", "entryPerArea": { - "NL": 303.24 + "NL": 79.94 } }, { - "deliveryStart": "2024-11-05T16:00:00Z", - "deliveryEnd": "2024-11-05T17:00:00Z", + "deliveryStart": "2025-10-01T02:15:00Z", + "deliveryEnd": "2025-10-01T02:30:00Z", "entryPerArea": { - "NL": 472.99 + "NL": 85.02 } }, { - "deliveryStart": "2024-11-05T17:00:00Z", - "deliveryEnd": "2024-11-05T18:00:00Z", + "deliveryStart": "2025-10-01T02:30:00Z", + "deliveryEnd": "2025-10-01T02:45:00Z", "entryPerArea": { - "NL": 431.02 + "NL": 83.89 } }, { - "deliveryStart": "2024-11-05T18:00:00Z", - "deliveryEnd": "2024-11-05T19:00:00Z", + "deliveryStart": "2025-10-01T02:45:00Z", + "deliveryEnd": "2025-10-01T03:00:00Z", "entryPerArea": { - "NL": 320.33 + "NL": 75.83 } }, { - "deliveryStart": "2024-11-05T19:00:00Z", - "deliveryEnd": "2024-11-05T20:00:00Z", + "deliveryStart": "2025-10-01T03:00:00Z", + "deliveryEnd": "2025-10-01T03:15:00Z", "entryPerArea": { - "NL": 169.7 + "NL": 75.01 } }, { - "deliveryStart": "2024-11-05T20:00:00Z", - "deliveryEnd": "2024-11-05T21:00:00Z", + "deliveryStart": "2025-10-01T03:15:00Z", + "deliveryEnd": "2025-10-01T03:30:00Z", "entryPerArea": { - "NL": 129.9 + "NL": 80.88 } }, { - "deliveryStart": "2024-11-05T21:00:00Z", - "deliveryEnd": "2024-11-05T22:00:00Z", + "deliveryStart": "2025-10-01T03:30:00Z", + "deliveryEnd": "2025-10-01T03:45:00Z", "entryPerArea": { - "NL": 117.77 + "NL": 88.18 } }, { - "deliveryStart": "2024-11-05T22:00:00Z", - "deliveryEnd": "2024-11-05T23:00:00Z", + "deliveryStart": "2025-10-01T03:45:00Z", + "deliveryEnd": "2025-10-01T04:00:00Z", "entryPerArea": { - "NL": 110.03 + "NL": 97.34 + } + }, + { + "deliveryStart": "2025-10-01T04:00:00Z", + "deliveryEnd": "2025-10-01T04:15:00Z", + "entryPerArea": { + "NL": 87.65 + } + }, + { + "deliveryStart": "2025-10-01T04:15:00Z", + "deliveryEnd": "2025-10-01T04:30:00Z", + "entryPerArea": { + "NL": 107.93 + } + }, + { + "deliveryStart": "2025-10-01T04:30:00Z", + "deliveryEnd": "2025-10-01T04:45:00Z", + "entryPerArea": { + "NL": 123.95 + } + }, + { + "deliveryStart": "2025-10-01T04:45:00Z", + "deliveryEnd": "2025-10-01T05:00:00Z", + "entryPerArea": { + "NL": 143.66 + } + }, + { + "deliveryStart": "2025-10-01T05:00:00Z", + "deliveryEnd": "2025-10-01T05:15:00Z", + "entryPerArea": { + "NL": 150.66 + } + }, + { + "deliveryStart": "2025-10-01T05:15:00Z", + "deliveryEnd": "2025-10-01T05:30:00Z", + "entryPerArea": { + "NL": 171.48 + } + }, + { + "deliveryStart": "2025-10-01T05:30:00Z", + "deliveryEnd": "2025-10-01T05:45:00Z", + "entryPerArea": { + "NL": 172.01 + } + }, + { + "deliveryStart": "2025-10-01T05:45:00Z", + "deliveryEnd": "2025-10-01T06:00:00Z", + "entryPerArea": { + "NL": 163.35 + } + }, + { + "deliveryStart": "2025-10-01T06:00:00Z", + "deliveryEnd": "2025-10-01T06:15:00Z", + "entryPerArea": { + "NL": 198.33 + } + }, + { + "deliveryStart": "2025-10-01T06:15:00Z", + "deliveryEnd": "2025-10-01T06:30:00Z", + "entryPerArea": { + "NL": 142.86 + } + }, + { + "deliveryStart": "2025-10-01T06:30:00Z", + "deliveryEnd": "2025-10-01T06:45:00Z", + "entryPerArea": { + "NL": 117.23 + } + }, + { + "deliveryStart": "2025-10-01T06:45:00Z", + "deliveryEnd": "2025-10-01T07:00:00Z", + "entryPerArea": { + "NL": 95.25 + } + }, + { + "deliveryStart": "2025-10-01T07:00:00Z", + "deliveryEnd": "2025-10-01T07:15:00Z", + "entryPerArea": { + "NL": 139.01 + } + }, + { + "deliveryStart": "2025-10-01T07:15:00Z", + "deliveryEnd": "2025-10-01T07:30:00Z", + "entryPerArea": { + "NL": 105.01 + } + }, + { + "deliveryStart": "2025-10-01T07:30:00Z", + "deliveryEnd": "2025-10-01T07:45:00Z", + "entryPerArea": { + "NL": 93.48 + } + }, + { + "deliveryStart": "2025-10-01T07:45:00Z", + "deliveryEnd": "2025-10-01T08:00:00Z", + "entryPerArea": { + "NL": 79.96 + } + }, + { + "deliveryStart": "2025-10-01T08:00:00Z", + "deliveryEnd": "2025-10-01T08:15:00Z", + "entryPerArea": { + "NL": 102.82 + } + }, + { + "deliveryStart": "2025-10-01T08:15:00Z", + "deliveryEnd": "2025-10-01T08:30:00Z", + "entryPerArea": { + "NL": 89.23 + } + }, + { + "deliveryStart": "2025-10-01T08:30:00Z", + "deliveryEnd": "2025-10-01T08:45:00Z", + "entryPerArea": { + "NL": 78.16 + } + }, + { + "deliveryStart": "2025-10-01T08:45:00Z", + "deliveryEnd": "2025-10-01T09:00:00Z", + "entryPerArea": { + "NL": 63.7 + } + }, + { + "deliveryStart": "2025-10-01T09:00:00Z", + "deliveryEnd": "2025-10-01T09:15:00Z", + "entryPerArea": { + "NL": 79.97 + } + }, + { + "deliveryStart": "2025-10-01T09:15:00Z", + "deliveryEnd": "2025-10-01T09:30:00Z", + "entryPerArea": { + "NL": 68.06 + } + }, + { + "deliveryStart": "2025-10-01T09:30:00Z", + "deliveryEnd": "2025-10-01T09:45:00Z", + "entryPerArea": { + "NL": 61.13 + } + }, + { + "deliveryStart": "2025-10-01T09:45:00Z", + "deliveryEnd": "2025-10-01T10:00:00Z", + "entryPerArea": { + "NL": 56.19 + } + }, + { + "deliveryStart": "2025-10-01T10:00:00Z", + "deliveryEnd": "2025-10-01T10:15:00Z", + "entryPerArea": { + "NL": 61.69 + } + }, + { + "deliveryStart": "2025-10-01T10:15:00Z", + "deliveryEnd": "2025-10-01T10:30:00Z", + "entryPerArea": { + "NL": 57.42 + } + }, + { + "deliveryStart": "2025-10-01T10:30:00Z", + "deliveryEnd": "2025-10-01T10:45:00Z", + "entryPerArea": { + "NL": 57.86 + } + }, + { + "deliveryStart": "2025-10-01T10:45:00Z", + "deliveryEnd": "2025-10-01T11:00:00Z", + "entryPerArea": { + "NL": 57.42 + } + }, + { + "deliveryStart": "2025-10-01T11:00:00Z", + "deliveryEnd": "2025-10-01T11:15:00Z", + "entryPerArea": { + "NL": 57.09 + } + }, + { + "deliveryStart": "2025-10-01T11:15:00Z", + "deliveryEnd": "2025-10-01T11:30:00Z", + "entryPerArea": { + "NL": 58.78 + } + }, + { + "deliveryStart": "2025-10-01T11:30:00Z", + "deliveryEnd": "2025-10-01T11:45:00Z", + "entryPerArea": { + "NL": 60.07 + } + }, + { + "deliveryStart": "2025-10-01T11:45:00Z", + "deliveryEnd": "2025-10-01T12:00:00Z", + "entryPerArea": { + "NL": 61.14 + } + }, + { + "deliveryStart": "2025-10-01T12:00:00Z", + "deliveryEnd": "2025-10-01T12:15:00Z", + "entryPerArea": { + "NL": 54.35 + } + }, + { + "deliveryStart": "2025-10-01T12:15:00Z", + "deliveryEnd": "2025-10-01T12:30:00Z", + "entryPerArea": { + "NL": 60.62 + } + }, + { + "deliveryStart": "2025-10-01T12:30:00Z", + "deliveryEnd": "2025-10-01T12:45:00Z", + "entryPerArea": { + "NL": 64.4 + } + }, + { + "deliveryStart": "2025-10-01T12:45:00Z", + "deliveryEnd": "2025-10-01T13:00:00Z", + "entryPerArea": { + "NL": 71.9 + } + }, + { + "deliveryStart": "2025-10-01T13:00:00Z", + "deliveryEnd": "2025-10-01T13:15:00Z", + "entryPerArea": { + "NL": 57.55 + } + }, + { + "deliveryStart": "2025-10-01T13:15:00Z", + "deliveryEnd": "2025-10-01T13:30:00Z", + "entryPerArea": { + "NL": 66.28 + } + }, + { + "deliveryStart": "2025-10-01T13:30:00Z", + "deliveryEnd": "2025-10-01T13:45:00Z", + "entryPerArea": { + "NL": 77.91 + } + }, + { + "deliveryStart": "2025-10-01T13:45:00Z", + "deliveryEnd": "2025-10-01T14:00:00Z", + "entryPerArea": { + "NL": 88.62 + } + }, + { + "deliveryStart": "2025-10-01T14:00:00Z", + "deliveryEnd": "2025-10-01T14:15:00Z", + "entryPerArea": { + "NL": 55.07 + } + }, + { + "deliveryStart": "2025-10-01T14:15:00Z", + "deliveryEnd": "2025-10-01T14:30:00Z", + "entryPerArea": { + "NL": 80.77 + } + }, + { + "deliveryStart": "2025-10-01T14:30:00Z", + "deliveryEnd": "2025-10-01T14:45:00Z", + "entryPerArea": { + "NL": 95.16 + } + }, + { + "deliveryStart": "2025-10-01T14:45:00Z", + "deliveryEnd": "2025-10-01T15:00:00Z", + "entryPerArea": { + "NL": 109.0 + } + }, + { + "deliveryStart": "2025-10-01T15:00:00Z", + "deliveryEnd": "2025-10-01T15:15:00Z", + "entryPerArea": { + "NL": 76.45 + } + }, + { + "deliveryStart": "2025-10-01T15:15:00Z", + "deliveryEnd": "2025-10-01T15:30:00Z", + "entryPerArea": { + "NL": 106.42 + } + }, + { + "deliveryStart": "2025-10-01T15:30:00Z", + "deliveryEnd": "2025-10-01T15:45:00Z", + "entryPerArea": { + "NL": 139.35 + } + }, + { + "deliveryStart": "2025-10-01T15:45:00Z", + "deliveryEnd": "2025-10-01T16:00:00Z", + "entryPerArea": { + "NL": 190.18 + } + }, + { + "deliveryStart": "2025-10-01T16:00:00Z", + "deliveryEnd": "2025-10-01T16:15:00Z", + "entryPerArea": { + "NL": 141.68 + } + }, + { + "deliveryStart": "2025-10-01T16:15:00Z", + "deliveryEnd": "2025-10-01T16:30:00Z", + "entryPerArea": { + "NL": 192.84 + } + }, + { + "deliveryStart": "2025-10-01T16:30:00Z", + "deliveryEnd": "2025-10-01T16:45:00Z", + "entryPerArea": { + "NL": 285.0 + } + }, + { + "deliveryStart": "2025-10-01T16:45:00Z", + "deliveryEnd": "2025-10-01T17:00:00Z", + "entryPerArea": { + "NL": 381.0 + } + }, + { + "deliveryStart": "2025-10-01T17:00:00Z", + "deliveryEnd": "2025-10-01T17:15:00Z", + "entryPerArea": { + "NL": 408.5 + } + }, + { + "deliveryStart": "2025-10-01T17:15:00Z", + "deliveryEnd": "2025-10-01T17:30:00Z", + "entryPerArea": { + "NL": 376.39 + } + }, + { + "deliveryStart": "2025-10-01T17:30:00Z", + "deliveryEnd": "2025-10-01T17:45:00Z", + "entryPerArea": { + "NL": 321.94 + } + }, + { + "deliveryStart": "2025-10-01T17:45:00Z", + "deliveryEnd": "2025-10-01T18:00:00Z", + "entryPerArea": { + "NL": 253.14 + } + }, + { + "deliveryStart": "2025-10-01T18:00:00Z", + "deliveryEnd": "2025-10-01T18:15:00Z", + "entryPerArea": { + "NL": 217.5 + } + }, + { + "deliveryStart": "2025-10-01T18:15:00Z", + "deliveryEnd": "2025-10-01T18:30:00Z", + "entryPerArea": { + "NL": 154.56 + } + }, + { + "deliveryStart": "2025-10-01T18:30:00Z", + "deliveryEnd": "2025-10-01T18:45:00Z", + "entryPerArea": { + "NL": 123.11 + } + }, + { + "deliveryStart": "2025-10-01T18:45:00Z", + "deliveryEnd": "2025-10-01T19:00:00Z", + "entryPerArea": { + "NL": 104.83 + } + }, + { + "deliveryStart": "2025-10-01T19:00:00Z", + "deliveryEnd": "2025-10-01T19:15:00Z", + "entryPerArea": { + "NL": 125.76 + } + }, + { + "deliveryStart": "2025-10-01T19:15:00Z", + "deliveryEnd": "2025-10-01T19:30:00Z", + "entryPerArea": { + "NL": 115.82 + } + }, + { + "deliveryStart": "2025-10-01T19:30:00Z", + "deliveryEnd": "2025-10-01T19:45:00Z", + "entryPerArea": { + "NL": 97.54 + } + }, + { + "deliveryStart": "2025-10-01T19:45:00Z", + "deliveryEnd": "2025-10-01T20:00:00Z", + "entryPerArea": { + "NL": 87.96 + } + }, + { + "deliveryStart": "2025-10-01T20:00:00Z", + "deliveryEnd": "2025-10-01T20:15:00Z", + "entryPerArea": { + "NL": 106.69 + } + }, + { + "deliveryStart": "2025-10-01T20:15:00Z", + "deliveryEnd": "2025-10-01T20:30:00Z", + "entryPerArea": { + "NL": 98.76 + } + }, + { + "deliveryStart": "2025-10-01T20:30:00Z", + "deliveryEnd": "2025-10-01T20:45:00Z", + "entryPerArea": { + "NL": 95.32 + } + }, + { + "deliveryStart": "2025-10-01T20:45:00Z", + "deliveryEnd": "2025-10-01T21:00:00Z", + "entryPerArea": { + "NL": 88.02 + } + }, + { + "deliveryStart": "2025-10-01T21:00:00Z", + "deliveryEnd": "2025-10-01T21:15:00Z", + "entryPerArea": { + "NL": 93.53 + } + }, + { + "deliveryStart": "2025-10-01T21:15:00Z", + "deliveryEnd": "2025-10-01T21:30:00Z", + "entryPerArea": { + "NL": 88.75 + } + }, + { + "deliveryStart": "2025-10-01T21:30:00Z", + "deliveryEnd": "2025-10-01T21:45:00Z", + "entryPerArea": { + "NL": 90.62 + } + }, + { + "deliveryStart": "2025-10-01T21:45:00Z", + "deliveryEnd": "2025-10-01T22:00:00Z", + "entryPerArea": { + "NL": 82.6 } } ], "blockPriceAggregates": [ { "blockName": "Off-peak 1", - "deliveryStart": "2024-11-04T23:00:00Z", - "deliveryEnd": "2024-11-05T07:00:00Z", + "deliveryStart": "2025-09-30T22:00:00Z", + "deliveryEnd": "2025-10-01T06:00:00Z", "averagePricePerArea": { "NL": { - "average": 98.96, - "min": 83.63, - "max": 135.99 + "average": 97.54, + "min": 75.01, + "max": 172.01 } } }, { "blockName": "Peak", - "deliveryStart": "2024-11-05T07:00:00Z", - "deliveryEnd": "2024-11-05T19:00:00Z", + "deliveryStart": "2025-10-01T06:00:00Z", + "deliveryEnd": "2025-10-01T18:00:00Z", "averagePricePerArea": { "NL": { - "average": 202.93, - "min": 94.92, - "max": 472.99 + "average": 120.76, + "min": 54.35, + "max": 408.5 } } }, { "blockName": "Off-peak 2", - "deliveryStart": "2024-11-05T19:00:00Z", - "deliveryEnd": "2024-11-05T23:00:00Z", + "deliveryStart": "2025-10-01T18:00:00Z", + "deliveryEnd": "2025-10-01T22:00:00Z", "averagePricePerArea": { "NL": { - "average": 131.85, - "min": 110.03, - "max": 169.7 + "average": 110.71, + "min": 82.6, + "max": 217.5 } } } @@ -223,7 +727,7 @@ "areaAverages": [ { "areaCode": "NL", - "price": 156.43 + "price": 111.34 } ] } diff --git a/tests/components/nordpool/fixtures/delivery_period_today.json b/tests/components/nordpool/fixtures/delivery_period_today.json index df48c32a9a97f6..ecd7b3868021ab 100644 --- a/tests/components/nordpool/fixtures/delivery_period_today.json +++ b/tests/components/nordpool/fixtures/delivery_period_today.json @@ -1,258 +1,834 @@ { - "deliveryDateCET": "2024-11-05", + "deliveryDateCET": "2025-10-01", "version": 3, - "updatedAt": "2024-11-04T12:15:03.9456464Z", + "updatedAt": "2025-09-30T12:08:16.4448023Z", "deliveryAreas": ["SE3", "SE4"], "market": "DayAhead", "multiAreaEntries": [ { - "deliveryStart": "2024-11-04T23:00:00Z", - "deliveryEnd": "2024-11-05T00:00:00Z", + "deliveryStart": "2025-09-30T22:00:00Z", + "deliveryEnd": "2025-09-30T22:15:00Z", "entryPerArea": { - "SE3": 250.73, - "SE4": 283.79 + "SE3": 556.68, + "SE4": 642.22 } }, { - "deliveryStart": "2024-11-05T00:00:00Z", - "deliveryEnd": "2024-11-05T01:00:00Z", + "deliveryStart": "2025-09-30T22:15:00Z", + "deliveryEnd": "2025-09-30T22:30:00Z", "entryPerArea": { - "SE3": 76.36, - "SE4": 81.36 + "SE3": 519.88, + "SE4": 600.12 } }, { - "deliveryStart": "2024-11-05T01:00:00Z", - "deliveryEnd": "2024-11-05T02:00:00Z", + "deliveryStart": "2025-09-30T22:30:00Z", + "deliveryEnd": "2025-09-30T22:45:00Z", "entryPerArea": { - "SE3": 73.92, - "SE4": 79.15 + "SE3": 508.28, + "SE4": 586.3 } }, { - "deliveryStart": "2024-11-05T02:00:00Z", - "deliveryEnd": "2024-11-05T03:00:00Z", + "deliveryStart": "2025-09-30T22:45:00Z", + "deliveryEnd": "2025-09-30T23:00:00Z", "entryPerArea": { - "SE3": 61.69, - "SE4": 65.19 + "SE3": 509.93, + "SE4": 589.62 } }, { - "deliveryStart": "2024-11-05T03:00:00Z", - "deliveryEnd": "2024-11-05T04:00:00Z", + "deliveryStart": "2025-09-30T23:00:00Z", + "deliveryEnd": "2025-09-30T23:15:00Z", "entryPerArea": { - "SE3": 64.6, - "SE4": 68.44 + "SE3": 501.64, + "SE4": 577.24 } }, { - "deliveryStart": "2024-11-05T04:00:00Z", - "deliveryEnd": "2024-11-05T05:00:00Z", + "deliveryStart": "2025-09-30T23:15:00Z", + "deliveryEnd": "2025-09-30T23:30:00Z", "entryPerArea": { - "SE3": 453.27, - "SE4": 516.71 + "SE3": 509.05, + "SE4": 585.42 } }, { - "deliveryStart": "2024-11-05T05:00:00Z", - "deliveryEnd": "2024-11-05T06:00:00Z", + "deliveryStart": "2025-09-30T23:30:00Z", + "deliveryEnd": "2025-09-30T23:45:00Z", "entryPerArea": { - "SE3": 996.28, - "SE4": 1240.85 + "SE3": 491.03, + "SE4": 567.18 } }, { - "deliveryStart": "2024-11-05T06:00:00Z", - "deliveryEnd": "2024-11-05T07:00:00Z", + "deliveryStart": "2025-09-30T23:45:00Z", + "deliveryEnd": "2025-10-01T00:00:00Z", "entryPerArea": { - "SE3": 1406.14, - "SE4": 1648.25 + "SE3": 442.07, + "SE4": 517.45 } }, { - "deliveryStart": "2024-11-05T07:00:00Z", - "deliveryEnd": "2024-11-05T08:00:00Z", + "deliveryStart": "2025-10-01T00:00:00Z", + "deliveryEnd": "2025-10-01T00:15:00Z", "entryPerArea": { - "SE3": 1346.54, - "SE4": 1570.5 + "SE3": 504.08, + "SE4": 580.55 } }, { - "deliveryStart": "2024-11-05T08:00:00Z", - "deliveryEnd": "2024-11-05T09:00:00Z", + "deliveryStart": "2025-10-01T00:15:00Z", + "deliveryEnd": "2025-10-01T00:30:00Z", "entryPerArea": { - "SE3": 1150.28, - "SE4": 1345.37 + "SE3": 504.85, + "SE4": 581.55 } }, { - "deliveryStart": "2024-11-05T09:00:00Z", - "deliveryEnd": "2024-11-05T10:00:00Z", + "deliveryStart": "2025-10-01T00:30:00Z", + "deliveryEnd": "2025-10-01T00:45:00Z", "entryPerArea": { - "SE3": 1031.32, - "SE4": 1206.51 + "SE3": 504.3, + "SE4": 580.78 } }, { - "deliveryStart": "2024-11-05T10:00:00Z", - "deliveryEnd": "2024-11-05T11:00:00Z", + "deliveryStart": "2025-10-01T00:45:00Z", + "deliveryEnd": "2025-10-01T01:00:00Z", "entryPerArea": { - "SE3": 927.37, - "SE4": 1085.8 + "SE3": 506.29, + "SE4": 583.1 } }, { - "deliveryStart": "2024-11-05T11:00:00Z", - "deliveryEnd": "2024-11-05T12:00:00Z", + "deliveryStart": "2025-10-01T01:00:00Z", + "deliveryEnd": "2025-10-01T01:15:00Z", "entryPerArea": { - "SE3": 925.05, - "SE4": 1081.72 + "SE3": 442.07, + "SE4": 515.46 } }, { - "deliveryStart": "2024-11-05T12:00:00Z", - "deliveryEnd": "2024-11-05T13:00:00Z", + "deliveryStart": "2025-10-01T01:15:00Z", + "deliveryEnd": "2025-10-01T01:30:00Z", "entryPerArea": { - "SE3": 949.49, - "SE4": 1130.38 + "SE3": 441.96, + "SE4": 517.23 } }, { - "deliveryStart": "2024-11-05T13:00:00Z", - "deliveryEnd": "2024-11-05T14:00:00Z", + "deliveryStart": "2025-10-01T01:30:00Z", + "deliveryEnd": "2025-10-01T01:45:00Z", "entryPerArea": { - "SE3": 1042.03, - "SE4": 1256.91 + "SE3": 442.07, + "SE4": 516.23 } }, { - "deliveryStart": "2024-11-05T14:00:00Z", - "deliveryEnd": "2024-11-05T15:00:00Z", + "deliveryStart": "2025-10-01T01:45:00Z", + "deliveryEnd": "2025-10-01T02:00:00Z", "entryPerArea": { - "SE3": 1258.89, - "SE4": 1765.82 + "SE3": 442.07, + "SE4": 516.23 } }, { - "deliveryStart": "2024-11-05T15:00:00Z", - "deliveryEnd": "2024-11-05T16:00:00Z", + "deliveryStart": "2025-10-01T02:00:00Z", + "deliveryEnd": "2025-10-01T02:15:00Z", "entryPerArea": { - "SE3": 1816.45, - "SE4": 2522.55 + "SE3": 441.96, + "SE4": 517.34 } }, { - "deliveryStart": "2024-11-05T16:00:00Z", - "deliveryEnd": "2024-11-05T17:00:00Z", + "deliveryStart": "2025-10-01T02:15:00Z", + "deliveryEnd": "2025-10-01T02:30:00Z", "entryPerArea": { - "SE3": 2512.65, - "SE4": 3533.03 + "SE3": 483.3, + "SE4": 559.11 } }, { - "deliveryStart": "2024-11-05T17:00:00Z", - "deliveryEnd": "2024-11-05T18:00:00Z", + "deliveryStart": "2025-10-01T02:30:00Z", + "deliveryEnd": "2025-10-01T02:45:00Z", "entryPerArea": { - "SE3": 1819.83, - "SE4": 2524.06 + "SE3": 484.29, + "SE4": 559.0 } }, { - "deliveryStart": "2024-11-05T18:00:00Z", - "deliveryEnd": "2024-11-05T19:00:00Z", + "deliveryStart": "2025-10-01T02:45:00Z", + "deliveryEnd": "2025-10-01T03:00:00Z", "entryPerArea": { - "SE3": 1011.77, + "SE3": 574.7, + "SE4": 659.35 + } + }, + { + "deliveryStart": "2025-10-01T03:00:00Z", + "deliveryEnd": "2025-10-01T03:15:00Z", + "entryPerArea": { + "SE3": 543.31, + "SE4": 631.95 + } + }, + { + "deliveryStart": "2025-10-01T03:15:00Z", + "deliveryEnd": "2025-10-01T03:30:00Z", + "entryPerArea": { + "SE3": 578.01, + "SE4": 671.18 + } + }, + { + "deliveryStart": "2025-10-01T03:30:00Z", + "deliveryEnd": "2025-10-01T03:45:00Z", + "entryPerArea": { + "SE3": 774.96, + "SE4": 893.1 + } + }, + { + "deliveryStart": "2025-10-01T03:45:00Z", + "deliveryEnd": "2025-10-01T04:00:00Z", + "entryPerArea": { + "SE3": 787.0, + "SE4": 909.79 + } + }, + { + "deliveryStart": "2025-10-01T04:00:00Z", + "deliveryEnd": "2025-10-01T04:15:00Z", + "entryPerArea": { + "SE3": 902.38, + "SE4": 1041.86 + } + }, + { + "deliveryStart": "2025-10-01T04:15:00Z", + "deliveryEnd": "2025-10-01T04:30:00Z", + "entryPerArea": { + "SE3": 1079.32, + "SE4": 1254.17 + } + }, + { + "deliveryStart": "2025-10-01T04:30:00Z", + "deliveryEnd": "2025-10-01T04:45:00Z", + "entryPerArea": { + "SE3": 1222.67, + "SE4": 1421.93 + } + }, + { + "deliveryStart": "2025-10-01T04:45:00Z", + "deliveryEnd": "2025-10-01T05:00:00Z", + "entryPerArea": { + "SE3": 1394.63, + "SE4": 1623.08 + } + }, + { + "deliveryStart": "2025-10-01T05:00:00Z", + "deliveryEnd": "2025-10-01T05:15:00Z", + "entryPerArea": { + "SE3": 1529.36, + "SE4": 1787.86 + } + }, + { + "deliveryStart": "2025-10-01T05:15:00Z", + "deliveryEnd": "2025-10-01T05:30:00Z", + "entryPerArea": { + "SE3": 1724.53, + "SE4": 2015.75 + } + }, + { + "deliveryStart": "2025-10-01T05:30:00Z", + "deliveryEnd": "2025-10-01T05:45:00Z", + "entryPerArea": { + "SE3": 1809.96, + "SE4": 2029.34 + } + }, + { + "deliveryStart": "2025-10-01T05:45:00Z", + "deliveryEnd": "2025-10-01T06:00:00Z", + "entryPerArea": { + "SE3": 1713.04, + "SE4": 1920.15 + } + }, + { + "deliveryStart": "2025-10-01T06:00:00Z", + "deliveryEnd": "2025-10-01T06:15:00Z", + "entryPerArea": { + "SE3": 1925.9, + "SE4": 2162.63 + } + }, + { + "deliveryStart": "2025-10-01T06:15:00Z", + "deliveryEnd": "2025-10-01T06:30:00Z", + "entryPerArea": { + "SE3": 1440.06, + "SE4": 1614.01 + } + }, + { + "deliveryStart": "2025-10-01T06:30:00Z", + "deliveryEnd": "2025-10-01T06:45:00Z", + "entryPerArea": { + "SE3": 1183.32, + "SE4": 1319.37 + } + }, + { + "deliveryStart": "2025-10-01T06:45:00Z", + "deliveryEnd": "2025-10-01T07:00:00Z", + "entryPerArea": { + "SE3": 962.95, + "SE4": 1068.71 + } + }, + { + "deliveryStart": "2025-10-01T07:00:00Z", + "deliveryEnd": "2025-10-01T07:15:00Z", + "entryPerArea": { + "SE3": 1402.04, + "SE4": 1569.92 + } + }, + { + "deliveryStart": "2025-10-01T07:15:00Z", + "deliveryEnd": "2025-10-01T07:30:00Z", + "entryPerArea": { + "SE3": 1060.65, + "SE4": 1178.46 + } + }, + { + "deliveryStart": "2025-10-01T07:30:00Z", + "deliveryEnd": "2025-10-01T07:45:00Z", + "entryPerArea": { + "SE3": 949.13, + "SE4": 1050.59 + } + }, + { + "deliveryStart": "2025-10-01T07:45:00Z", + "deliveryEnd": "2025-10-01T08:00:00Z", + "entryPerArea": { + "SE3": 841.82, + "SE4": 938.3 + } + }, + { + "deliveryStart": "2025-10-01T08:00:00Z", + "deliveryEnd": "2025-10-01T08:15:00Z", + "entryPerArea": { + "SE3": 1037.44, + "SE4": 1141.44 + } + }, + { + "deliveryStart": "2025-10-01T08:15:00Z", + "deliveryEnd": "2025-10-01T08:30:00Z", + "entryPerArea": { + "SE3": 950.13, + "SE4": 1041.64 + } + }, + { + "deliveryStart": "2025-10-01T08:30:00Z", + "deliveryEnd": "2025-10-01T08:45:00Z", + "entryPerArea": { + "SE3": 826.13, + "SE4": 905.04 + } + }, + { + "deliveryStart": "2025-10-01T08:45:00Z", + "deliveryEnd": "2025-10-01T09:00:00Z", + "entryPerArea": { + "SE3": 684.55, + "SE4": 754.62 + } + }, + { + "deliveryStart": "2025-10-01T09:00:00Z", + "deliveryEnd": "2025-10-01T09:15:00Z", + "entryPerArea": { + "SE3": 861.6, + "SE4": 936.09 + } + }, + { + "deliveryStart": "2025-10-01T09:15:00Z", + "deliveryEnd": "2025-10-01T09:30:00Z", + "entryPerArea": { + "SE3": 722.79, + "SE4": 799.6 + } + }, + { + "deliveryStart": "2025-10-01T09:30:00Z", + "deliveryEnd": "2025-10-01T09:45:00Z", + "entryPerArea": { + "SE3": 640.57, + "SE4": 718.59 + } + }, + { + "deliveryStart": "2025-10-01T09:45:00Z", + "deliveryEnd": "2025-10-01T10:00:00Z", + "entryPerArea": { + "SE3": 607.74, + "SE4": 683.12 + } + }, + { + "deliveryStart": "2025-10-01T10:00:00Z", + "deliveryEnd": "2025-10-01T10:15:00Z", + "entryPerArea": { + "SE3": 674.05, + "SE4": 752.41 + } + }, + { + "deliveryStart": "2025-10-01T10:15:00Z", + "deliveryEnd": "2025-10-01T10:30:00Z", + "entryPerArea": { + "SE3": 638.58, + "SE4": 717.49 + } + }, + { + "deliveryStart": "2025-10-01T10:30:00Z", + "deliveryEnd": "2025-10-01T10:45:00Z", + "entryPerArea": { + "SE3": 638.47, + "SE4": 719.81 + } + }, + { + "deliveryStart": "2025-10-01T10:45:00Z", + "deliveryEnd": "2025-10-01T11:00:00Z", + "entryPerArea": { + "SE3": 634.82, + "SE4": 717.16 + } + }, + { + "deliveryStart": "2025-10-01T11:00:00Z", + "deliveryEnd": "2025-10-01T11:15:00Z", + "entryPerArea": { + "SE3": 637.36, + "SE4": 721.58 + } + }, + { + "deliveryStart": "2025-10-01T11:15:00Z", + "deliveryEnd": "2025-10-01T11:30:00Z", + "entryPerArea": { + "SE3": 660.68, + "SE4": 746.33 + } + }, + { + "deliveryStart": "2025-10-01T11:30:00Z", + "deliveryEnd": "2025-10-01T11:45:00Z", + "entryPerArea": { + "SE3": 679.14, + "SE4": 766.45 + } + }, + { + "deliveryStart": "2025-10-01T11:45:00Z", + "deliveryEnd": "2025-10-01T12:00:00Z", + "entryPerArea": { + "SE3": 694.61, + "SE4": 782.91 + } + }, + { + "deliveryStart": "2025-10-01T12:00:00Z", + "deliveryEnd": "2025-10-01T12:15:00Z", + "entryPerArea": { + "SE3": 622.33, + "SE4": 708.87 + } + }, + { + "deliveryStart": "2025-10-01T12:15:00Z", + "deliveryEnd": "2025-10-01T12:30:00Z", + "entryPerArea": { + "SE3": 685.44, + "SE4": 775.84 + } + }, + { + "deliveryStart": "2025-10-01T12:30:00Z", + "deliveryEnd": "2025-10-01T12:45:00Z", + "entryPerArea": { + "SE3": 732.85, + "SE4": 826.57 + } + }, + { + "deliveryStart": "2025-10-01T12:45:00Z", + "deliveryEnd": "2025-10-01T13:00:00Z", + "entryPerArea": { + "SE3": 801.92, + "SE4": 901.28 + } + }, + { + "deliveryStart": "2025-10-01T13:00:00Z", + "deliveryEnd": "2025-10-01T13:15:00Z", + "entryPerArea": { + "SE3": 629.4, + "SE4": 717.93 + } + }, + { + "deliveryStart": "2025-10-01T13:15:00Z", + "deliveryEnd": "2025-10-01T13:30:00Z", + "entryPerArea": { + "SE3": 729.53, + "SE4": 825.46 + } + }, + { + "deliveryStart": "2025-10-01T13:30:00Z", + "deliveryEnd": "2025-10-01T13:45:00Z", + "entryPerArea": { + "SE3": 884.81, + "SE4": 983.95 + } + }, + { + "deliveryStart": "2025-10-01T13:45:00Z", + "deliveryEnd": "2025-10-01T14:00:00Z", + "entryPerArea": { + "SE3": 984.94, + "SE4": 1089.71 + } + }, + { + "deliveryStart": "2025-10-01T14:00:00Z", + "deliveryEnd": "2025-10-01T14:15:00Z", + "entryPerArea": { + "SE3": 615.26, + "SE4": 703.12 + } + }, + { + "deliveryStart": "2025-10-01T14:15:00Z", + "deliveryEnd": "2025-10-01T14:30:00Z", + "entryPerArea": { + "SE3": 902.94, + "SE4": 1002.74 + } + }, + { + "deliveryStart": "2025-10-01T14:30:00Z", + "deliveryEnd": "2025-10-01T14:45:00Z", + "entryPerArea": { + "SE3": 1043.85, + "SE4": 1158.35 + } + }, + { + "deliveryStart": "2025-10-01T14:45:00Z", + "deliveryEnd": "2025-10-01T15:00:00Z", + "entryPerArea": { + "SE3": 1075.12, + "SE4": 1194.15 + } + }, + { + "deliveryStart": "2025-10-01T15:00:00Z", + "deliveryEnd": "2025-10-01T15:15:00Z", + "entryPerArea": { + "SE3": 980.52, + "SE4": 1089.38 + } + }, + { + "deliveryStart": "2025-10-01T15:15:00Z", + "deliveryEnd": "2025-10-01T15:30:00Z", + "entryPerArea": { + "SE3": 1162.66, + "SE4": 1300.14 + } + }, + { + "deliveryStart": "2025-10-01T15:30:00Z", + "deliveryEnd": "2025-10-01T15:45:00Z", + "entryPerArea": { + "SE3": 1453.87, + "SE4": 1628.6 + } + }, + { + "deliveryStart": "2025-10-01T15:45:00Z", + "deliveryEnd": "2025-10-01T16:00:00Z", + "entryPerArea": { + "SE3": 1955.96, + "SE4": 2193.35 + } + }, + { + "deliveryStart": "2025-10-01T16:00:00Z", + "deliveryEnd": "2025-10-01T16:15:00Z", + "entryPerArea": { + "SE3": 1423.48, + "SE4": 1623.74 + } + }, + { + "deliveryStart": "2025-10-01T16:15:00Z", + "deliveryEnd": "2025-10-01T16:30:00Z", + "entryPerArea": { + "SE3": 1900.04, + "SE4": 2199.98 + } + }, + { + "deliveryStart": "2025-10-01T16:30:00Z", + "deliveryEnd": "2025-10-01T16:45:00Z", + "entryPerArea": { + "SE3": 2611.11, + "SE4": 3031.08 + } + }, + { + "deliveryStart": "2025-10-01T16:45:00Z", + "deliveryEnd": "2025-10-01T17:00:00Z", + "entryPerArea": { + "SE3": 3467.41, + "SE4": 4029.51 + } + }, + { + "deliveryStart": "2025-10-01T17:00:00Z", + "deliveryEnd": "2025-10-01T17:15:00Z", + "entryPerArea": { + "SE3": 3828.03, + "SE4": 4442.74 + } + }, + { + "deliveryStart": "2025-10-01T17:15:00Z", + "deliveryEnd": "2025-10-01T17:30:00Z", + "entryPerArea": { + "SE3": 3429.83, + "SE4": 3982.21 + } + }, + { + "deliveryStart": "2025-10-01T17:30:00Z", + "deliveryEnd": "2025-10-01T17:45:00Z", + "entryPerArea": { + "SE3": 2934.38, + "SE4": 3405.74 + } + }, + { + "deliveryStart": "2025-10-01T17:45:00Z", + "deliveryEnd": "2025-10-01T18:00:00Z", + "entryPerArea": { + "SE3": 2308.07, + "SE4": 2677.64 + } + }, + { + "deliveryStart": "2025-10-01T18:00:00Z", + "deliveryEnd": "2025-10-01T18:15:00Z", + "entryPerArea": { + "SE3": 1997.96, "SE4": 0.0 } }, { - "deliveryStart": "2024-11-05T19:00:00Z", - "deliveryEnd": "2024-11-05T20:00:00Z", + "deliveryStart": "2025-10-01T18:15:00Z", + "deliveryEnd": "2025-10-01T18:30:00Z", + "entryPerArea": { + "SE3": 1424.03, + "SE4": 1646.17 + } + }, + { + "deliveryStart": "2025-10-01T18:30:00Z", + "deliveryEnd": "2025-10-01T18:45:00Z", + "entryPerArea": { + "SE3": 1216.81, + "SE4": 1388.11 + } + }, + { + "deliveryStart": "2025-10-01T18:45:00Z", + "deliveryEnd": "2025-10-01T19:00:00Z", + "entryPerArea": { + "SE3": 1070.15, + "SE4": 1204.65 + } + }, + { + "deliveryStart": "2025-10-01T19:00:00Z", + "deliveryEnd": "2025-10-01T19:15:00Z", + "entryPerArea": { + "SE3": 1218.14, + "SE4": 1405.02 + } + }, + { + "deliveryStart": "2025-10-01T19:15:00Z", + "deliveryEnd": "2025-10-01T19:30:00Z", + "entryPerArea": { + "SE3": 1135.8, + "SE4": 1309.42 + } + }, + { + "deliveryStart": "2025-10-01T19:30:00Z", + "deliveryEnd": "2025-10-01T19:45:00Z", + "entryPerArea": { + "SE3": 959.96, + "SE4": 1115.69 + } + }, + { + "deliveryStart": "2025-10-01T19:45:00Z", + "deliveryEnd": "2025-10-01T20:00:00Z", + "entryPerArea": { + "SE3": 913.66, + "SE4": 1064.52 + } + }, + { + "deliveryStart": "2025-10-01T20:00:00Z", + "deliveryEnd": "2025-10-01T20:15:00Z", + "entryPerArea": { + "SE3": 1001.63, + "SE4": 1161.22 + } + }, + { + "deliveryStart": "2025-10-01T20:15:00Z", + "deliveryEnd": "2025-10-01T20:30:00Z", + "entryPerArea": { + "SE3": 933.0, + "SE4": 1083.08 + } + }, + { + "deliveryStart": "2025-10-01T20:30:00Z", + "deliveryEnd": "2025-10-01T20:45:00Z", + "entryPerArea": { + "SE3": 874.53, + "SE4": 1017.66 + } + }, + { + "deliveryStart": "2025-10-01T20:45:00Z", + "deliveryEnd": "2025-10-01T21:00:00Z", + "entryPerArea": { + "SE3": 821.71, + "SE4": 955.32 + } + }, + { + "deliveryStart": "2025-10-01T21:00:00Z", + "deliveryEnd": "2025-10-01T21:15:00Z", "entryPerArea": { - "SE3": 835.53, - "SE4": 1112.57 + "SE3": 860.5, + "SE4": 997.32 } }, { - "deliveryStart": "2024-11-05T20:00:00Z", - "deliveryEnd": "2024-11-05T21:00:00Z", + "deliveryStart": "2025-10-01T21:15:00Z", + "deliveryEnd": "2025-10-01T21:30:00Z", "entryPerArea": { - "SE3": 796.19, - "SE4": 1051.69 + "SE3": 840.16, + "SE4": 977.87 } }, { - "deliveryStart": "2024-11-05T21:00:00Z", - "deliveryEnd": "2024-11-05T22:00:00Z", + "deliveryStart": "2025-10-01T21:30:00Z", + "deliveryEnd": "2025-10-01T21:45:00Z", "entryPerArea": { - "SE3": 522.3, - "SE4": 662.44 + "SE3": 820.05, + "SE4": 954.66 } }, { - "deliveryStart": "2024-11-05T22:00:00Z", - "deliveryEnd": "2024-11-05T23:00:00Z", + "deliveryStart": "2025-10-01T21:45:00Z", + "deliveryEnd": "2025-10-01T22:00:00Z", "entryPerArea": { - "SE3": 289.14, - "SE4": 349.21 + "SE3": 785.68, + "SE4": 912.22 } } ], "blockPriceAggregates": [ { "blockName": "Off-peak 1", - "deliveryStart": "2024-11-04T23:00:00Z", - "deliveryEnd": "2024-11-05T07:00:00Z", + "deliveryStart": "2025-09-30T22:00:00Z", + "deliveryEnd": "2025-10-01T06:00:00Z", "averagePricePerArea": { "SE3": { - "average": 422.87, - "min": 61.69, - "max": 1406.14 + "average": 745.93, + "min": 441.96, + "max": 1809.96 }, "SE4": { - "average": 497.97, - "min": 65.19, - "max": 1648.25 + "average": 860.99, + "min": 515.46, + "max": 2029.34 } } }, { "blockName": "Peak", - "deliveryStart": "2024-11-05T07:00:00Z", - "deliveryEnd": "2024-11-05T19:00:00Z", + "deliveryStart": "2025-10-01T06:00:00Z", + "deliveryEnd": "2025-10-01T18:00:00Z", "averagePricePerArea": { "SE3": { - "average": 1315.97, - "min": 925.05, - "max": 2512.65 + "average": 1219.13, + "min": 607.74, + "max": 3828.03 }, "SE4": { - "average": 1735.59, - "min": 1081.72, - "max": 3533.03 + "average": 1381.22, + "min": 683.12, + "max": 4442.74 } } }, { "blockName": "Off-peak 2", - "deliveryStart": "2024-11-05T19:00:00Z", - "deliveryEnd": "2024-11-05T23:00:00Z", + "deliveryStart": "2025-10-01T18:00:00Z", + "deliveryEnd": "2025-10-01T22:00:00Z", "averagePricePerArea": { "SE3": { - "average": 610.79, - "min": 289.14, - "max": 835.53 + "average": 1054.61, + "min": 785.68, + "max": 1997.96 }, "SE4": { - "average": 793.98, - "min": 349.21, - "max": 1112.57 + "average": 1219.07, + "min": 912.22, + "max": 2312.16 } } } ], "currency": "SEK", - "exchangeRate": 11.6402, + "exchangeRate": 11.05186, "areaStates": [ { "state": "Final", @@ -262,11 +838,11 @@ "areaAverages": [ { "areaCode": "SE3", - "price": 900.74 + "price": 1033.98 }, { "areaCode": "SE4", - "price": 1166.12 + "price": 1180.78 } ] } diff --git a/tests/components/nordpool/fixtures/delivery_period_tomorrow.json b/tests/components/nordpool/fixtures/delivery_period_tomorrow.json index abaa24e93ede6b..0e64088d33bfe2 100644 --- a/tests/components/nordpool/fixtures/delivery_period_tomorrow.json +++ b/tests/components/nordpool/fixtures/delivery_period_tomorrow.json @@ -1,258 +1,834 @@ { - "deliveryDateCET": "2024-11-06", + "deliveryDateCET": "2025-10-02", "version": 3, - "updatedAt": "2024-11-05T12:12:51.9853434Z", + "updatedAt": "2025-10-01T11:25:06.1484362Z", "deliveryAreas": ["SE3", "SE4"], "market": "DayAhead", "multiAreaEntries": [ { - "deliveryStart": "2024-11-05T23:00:00Z", - "deliveryEnd": "2024-11-06T00:00:00Z", + "deliveryStart": "2025-10-01T22:00:00Z", + "deliveryEnd": "2025-10-01T22:15:00Z", "entryPerArea": { - "SE3": 126.66, - "SE4": 275.6 + "SE3": 933.22, + "SE4": 1062.32 } }, { - "deliveryStart": "2024-11-06T00:00:00Z", - "deliveryEnd": "2024-11-06T01:00:00Z", + "deliveryStart": "2025-10-01T22:15:00Z", + "deliveryEnd": "2025-10-01T22:30:00Z", "entryPerArea": { - "SE3": 74.06, - "SE4": 157.34 + "SE3": 854.22, + "SE4": 971.95 } }, { - "deliveryStart": "2024-11-06T01:00:00Z", - "deliveryEnd": "2024-11-06T02:00:00Z", + "deliveryStart": "2025-10-01T22:30:00Z", + "deliveryEnd": "2025-10-01T22:45:00Z", "entryPerArea": { - "SE3": 78.38, - "SE4": 165.62 + "SE3": 809.54, + "SE4": 919.1 } }, { - "deliveryStart": "2024-11-06T02:00:00Z", - "deliveryEnd": "2024-11-06T03:00:00Z", + "deliveryStart": "2025-10-01T22:45:00Z", + "deliveryEnd": "2025-10-01T23:00:00Z", "entryPerArea": { - "SE3": 92.37, - "SE4": 196.17 + "SE3": 811.74, + "SE4": 922.63 } }, { - "deliveryStart": "2024-11-06T03:00:00Z", - "deliveryEnd": "2024-11-06T04:00:00Z", + "deliveryStart": "2025-10-01T23:00:00Z", + "deliveryEnd": "2025-10-01T23:15:00Z", "entryPerArea": { - "SE3": 99.14, - "SE4": 190.58 + "SE3": 835.13, + "SE4": 950.99 } }, { - "deliveryStart": "2024-11-06T04:00:00Z", - "deliveryEnd": "2024-11-06T05:00:00Z", + "deliveryStart": "2025-10-01T23:15:00Z", + "deliveryEnd": "2025-10-01T23:30:00Z", "entryPerArea": { - "SE3": 447.51, - "SE4": 932.93 + "SE3": 828.85, + "SE4": 942.82 } }, { - "deliveryStart": "2024-11-06T05:00:00Z", - "deliveryEnd": "2024-11-06T06:00:00Z", + "deliveryStart": "2025-10-01T23:30:00Z", + "deliveryEnd": "2025-10-01T23:45:00Z", "entryPerArea": { - "SE3": 641.47, - "SE4": 1284.69 + "SE3": 796.63, + "SE4": 903.54 } }, { - "deliveryStart": "2024-11-06T06:00:00Z", - "deliveryEnd": "2024-11-06T07:00:00Z", + "deliveryStart": "2025-10-01T23:45:00Z", + "deliveryEnd": "2025-10-02T00:00:00Z", "entryPerArea": { - "SE3": 1820.5, - "SE4": 2449.96 + "SE3": 706.7, + "SE4": 799.61 } }, { - "deliveryStart": "2024-11-06T07:00:00Z", - "deliveryEnd": "2024-11-06T08:00:00Z", + "deliveryStart": "2025-10-02T00:00:00Z", + "deliveryEnd": "2025-10-02T00:15:00Z", "entryPerArea": { - "SE3": 1723.0, - "SE4": 2244.22 + "SE3": 695.23, + "SE4": 786.81 } }, { - "deliveryStart": "2024-11-06T08:00:00Z", - "deliveryEnd": "2024-11-06T09:00:00Z", + "deliveryStart": "2025-10-02T00:15:00Z", + "deliveryEnd": "2025-10-02T00:30:00Z", "entryPerArea": { - "SE3": 1298.57, - "SE4": 1643.45 + "SE3": 695.12, + "SE4": 783.83 } }, { - "deliveryStart": "2024-11-06T09:00:00Z", - "deliveryEnd": "2024-11-06T10:00:00Z", + "deliveryStart": "2025-10-02T00:30:00Z", + "deliveryEnd": "2025-10-02T00:45:00Z", "entryPerArea": { - "SE3": 1099.25, - "SE4": 1507.23 + "SE3": 684.86, + "SE4": 771.8 } }, { - "deliveryStart": "2024-11-06T10:00:00Z", - "deliveryEnd": "2024-11-06T11:00:00Z", + "deliveryStart": "2025-10-02T00:45:00Z", + "deliveryEnd": "2025-10-02T01:00:00Z", "entryPerArea": { - "SE3": 903.31, - "SE4": 1362.84 + "SE3": 673.05, + "SE4": 758.78 } }, { - "deliveryStart": "2024-11-06T11:00:00Z", - "deliveryEnd": "2024-11-06T12:00:00Z", + "deliveryStart": "2025-10-02T01:00:00Z", + "deliveryEnd": "2025-10-02T01:15:00Z", "entryPerArea": { - "SE3": 959.99, - "SE4": 1376.13 + "SE3": 695.01, + "SE4": 791.22 } }, { - "deliveryStart": "2024-11-06T12:00:00Z", - "deliveryEnd": "2024-11-06T13:00:00Z", + "deliveryStart": "2025-10-02T01:15:00Z", + "deliveryEnd": "2025-10-02T01:30:00Z", "entryPerArea": { - "SE3": 1186.61, - "SE4": 1449.96 + "SE3": 693.35, + "SE4": 789.12 } }, { - "deliveryStart": "2024-11-06T13:00:00Z", - "deliveryEnd": "2024-11-06T14:00:00Z", + "deliveryStart": "2025-10-02T01:30:00Z", + "deliveryEnd": "2025-10-02T01:45:00Z", "entryPerArea": { - "SE3": 1307.67, - "SE4": 1608.35 + "SE3": 702.4, + "SE4": 799.61 } }, { - "deliveryStart": "2024-11-06T14:00:00Z", - "deliveryEnd": "2024-11-06T15:00:00Z", + "deliveryStart": "2025-10-02T01:45:00Z", + "deliveryEnd": "2025-10-02T02:00:00Z", "entryPerArea": { - "SE3": 1385.46, - "SE4": 2110.8 + "SE3": 749.4, + "SE4": 853.45 } }, { - "deliveryStart": "2024-11-06T15:00:00Z", - "deliveryEnd": "2024-11-06T16:00:00Z", + "deliveryStart": "2025-10-02T02:00:00Z", + "deliveryEnd": "2025-10-02T02:15:00Z", "entryPerArea": { - "SE3": 1366.8, - "SE4": 3031.25 + "SE3": 796.85, + "SE4": 907.4 } }, { - "deliveryStart": "2024-11-06T16:00:00Z", - "deliveryEnd": "2024-11-06T17:00:00Z", + "deliveryStart": "2025-10-02T02:15:00Z", + "deliveryEnd": "2025-10-02T02:30:00Z", "entryPerArea": { - "SE3": 2366.57, - "SE4": 5511.77 + "SE3": 811.19, + "SE4": 924.07 } }, { - "deliveryStart": "2024-11-06T17:00:00Z", - "deliveryEnd": "2024-11-06T18:00:00Z", + "deliveryStart": "2025-10-02T02:30:00Z", + "deliveryEnd": "2025-10-02T02:45:00Z", "entryPerArea": { - "SE3": 1481.92, - "SE4": 3351.64 + "SE3": 803.8, + "SE4": 916.23 } }, { - "deliveryStart": "2024-11-06T18:00:00Z", - "deliveryEnd": "2024-11-06T19:00:00Z", + "deliveryStart": "2025-10-02T02:45:00Z", + "deliveryEnd": "2025-10-02T03:00:00Z", "entryPerArea": { - "SE3": 1082.69, - "SE4": 2484.95 + "SE3": 839.11, + "SE4": 953.3 } }, { - "deliveryStart": "2024-11-06T19:00:00Z", - "deliveryEnd": "2024-11-06T20:00:00Z", + "deliveryStart": "2025-10-02T03:00:00Z", + "deliveryEnd": "2025-10-02T03:15:00Z", "entryPerArea": { - "SE3": 716.82, - "SE4": 1624.33 + "SE3": 825.2, + "SE4": 943.15 } }, { - "deliveryStart": "2024-11-06T20:00:00Z", - "deliveryEnd": "2024-11-06T21:00:00Z", + "deliveryStart": "2025-10-02T03:15:00Z", + "deliveryEnd": "2025-10-02T03:30:00Z", "entryPerArea": { - "SE3": 583.16, - "SE4": 1306.27 + "SE3": 838.78, + "SE4": 958.93 } }, { - "deliveryStart": "2024-11-06T21:00:00Z", - "deliveryEnd": "2024-11-06T22:00:00Z", + "deliveryStart": "2025-10-02T03:30:00Z", + "deliveryEnd": "2025-10-02T03:45:00Z", "entryPerArea": { - "SE3": 523.09, - "SE4": 1142.99 + "SE3": 906.19, + "SE4": 1030.65 } }, { - "deliveryStart": "2024-11-06T22:00:00Z", - "deliveryEnd": "2024-11-06T23:00:00Z", + "deliveryStart": "2025-10-02T03:45:00Z", + "deliveryEnd": "2025-10-02T04:00:00Z", "entryPerArea": { - "SE3": 250.64, - "SE4": 539.42 + "SE3": 1057.79, + "SE4": 1195.82 + } + }, + { + "deliveryStart": "2025-10-02T04:00:00Z", + "deliveryEnd": "2025-10-02T04:15:00Z", + "entryPerArea": { + "SE3": 912.15, + "SE4": 1040.8 + } + }, + { + "deliveryStart": "2025-10-02T04:15:00Z", + "deliveryEnd": "2025-10-02T04:30:00Z", + "entryPerArea": { + "SE3": 1131.28, + "SE4": 1283.43 + } + }, + { + "deliveryStart": "2025-10-02T04:30:00Z", + "deliveryEnd": "2025-10-02T04:45:00Z", + "entryPerArea": { + "SE3": 1294.68, + "SE4": 1468.91 + } + }, + { + "deliveryStart": "2025-10-02T04:45:00Z", + "deliveryEnd": "2025-10-02T05:00:00Z", + "entryPerArea": { + "SE3": 1625.8, + "SE4": 1845.81 + } + }, + { + "deliveryStart": "2025-10-02T05:00:00Z", + "deliveryEnd": "2025-10-02T05:15:00Z", + "entryPerArea": { + "SE3": 1649.31, + "SE4": 1946.77 + } + }, + { + "deliveryStart": "2025-10-02T05:15:00Z", + "deliveryEnd": "2025-10-02T05:30:00Z", + "entryPerArea": { + "SE3": 1831.25, + "SE4": 2182.34 + } + }, + { + "deliveryStart": "2025-10-02T05:30:00Z", + "deliveryEnd": "2025-10-02T05:45:00Z", + "entryPerArea": { + "SE3": 1743.31, + "SE4": 2063.4 + } + }, + { + "deliveryStart": "2025-10-02T05:45:00Z", + "deliveryEnd": "2025-10-02T06:00:00Z", + "entryPerArea": { + "SE3": 1545.04, + "SE4": 1803.33 + } + }, + { + "deliveryStart": "2025-10-02T06:00:00Z", + "deliveryEnd": "2025-10-02T06:15:00Z", + "entryPerArea": { + "SE3": 1783.47, + "SE4": 2080.72 + } + }, + { + "deliveryStart": "2025-10-02T06:15:00Z", + "deliveryEnd": "2025-10-02T06:30:00Z", + "entryPerArea": { + "SE3": 1470.89, + "SE4": 1675.23 + } + }, + { + "deliveryStart": "2025-10-02T06:30:00Z", + "deliveryEnd": "2025-10-02T06:45:00Z", + "entryPerArea": { + "SE3": 1191.08, + "SE4": 1288.06 + } + }, + { + "deliveryStart": "2025-10-02T06:45:00Z", + "deliveryEnd": "2025-10-02T07:00:00Z", + "entryPerArea": { + "SE3": 1012.22, + "SE4": 1112.19 + } + }, + { + "deliveryStart": "2025-10-02T07:00:00Z", + "deliveryEnd": "2025-10-02T07:15:00Z", + "entryPerArea": { + "SE3": 1278.69, + "SE4": 1375.67 + } + }, + { + "deliveryStart": "2025-10-02T07:15:00Z", + "deliveryEnd": "2025-10-02T07:30:00Z", + "entryPerArea": { + "SE3": 1170.12, + "SE4": 1258.61 + } + }, + { + "deliveryStart": "2025-10-02T07:30:00Z", + "deliveryEnd": "2025-10-02T07:45:00Z", + "entryPerArea": { + "SE3": 937.09, + "SE4": 1021.93 + } + }, + { + "deliveryStart": "2025-10-02T07:45:00Z", + "deliveryEnd": "2025-10-02T08:00:00Z", + "entryPerArea": { + "SE3": 815.94, + "SE4": 900.67 + } + }, + { + "deliveryStart": "2025-10-02T08:00:00Z", + "deliveryEnd": "2025-10-02T08:15:00Z", + "entryPerArea": { + "SE3": 1044.66, + "SE4": 1135.25 + } + }, + { + "deliveryStart": "2025-10-02T08:15:00Z", + "deliveryEnd": "2025-10-02T08:30:00Z", + "entryPerArea": { + "SE3": 1020.61, + "SE4": 1112.74 + } + }, + { + "deliveryStart": "2025-10-02T08:30:00Z", + "deliveryEnd": "2025-10-02T08:45:00Z", + "entryPerArea": { + "SE3": 866.14, + "SE4": 953.53 + } + }, + { + "deliveryStart": "2025-10-02T08:45:00Z", + "deliveryEnd": "2025-10-02T09:00:00Z", + "entryPerArea": { + "SE3": 774.34, + "SE4": 860.18 + } + }, + { + "deliveryStart": "2025-10-02T09:00:00Z", + "deliveryEnd": "2025-10-02T09:15:00Z", + "entryPerArea": { + "SE3": 928.26, + "SE4": 1020.39 + } + }, + { + "deliveryStart": "2025-10-02T09:15:00Z", + "deliveryEnd": "2025-10-02T09:30:00Z", + "entryPerArea": { + "SE3": 834.47, + "SE4": 922.96 + } + }, + { + "deliveryStart": "2025-10-02T09:30:00Z", + "deliveryEnd": "2025-10-02T09:45:00Z", + "entryPerArea": { + "SE3": 712.33, + "SE4": 794.64 + } + }, + { + "deliveryStart": "2025-10-02T09:45:00Z", + "deliveryEnd": "2025-10-02T10:00:00Z", + "entryPerArea": { + "SE3": 646.46, + "SE4": 725.9 + } + }, + { + "deliveryStart": "2025-10-02T10:00:00Z", + "deliveryEnd": "2025-10-02T10:15:00Z", + "entryPerArea": { + "SE3": 692.91, + "SE4": 773.9 + } + }, + { + "deliveryStart": "2025-10-02T10:15:00Z", + "deliveryEnd": "2025-10-02T10:30:00Z", + "entryPerArea": { + "SE3": 627.59, + "SE4": 706.59 + } + }, + { + "deliveryStart": "2025-10-02T10:30:00Z", + "deliveryEnd": "2025-10-02T10:45:00Z", + "entryPerArea": { + "SE3": 630.02, + "SE4": 708.14 + } + }, + { + "deliveryStart": "2025-10-02T10:45:00Z", + "deliveryEnd": "2025-10-02T11:00:00Z", + "entryPerArea": { + "SE3": 625.94, + "SE4": 703.61 + } + }, + { + "deliveryStart": "2025-10-02T11:00:00Z", + "deliveryEnd": "2025-10-02T11:15:00Z", + "entryPerArea": { + "SE3": 563.38, + "SE4": 635.76 + } + }, + { + "deliveryStart": "2025-10-02T11:15:00Z", + "deliveryEnd": "2025-10-02T11:30:00Z", + "entryPerArea": { + "SE3": 588.42, + "SE4": 663.12 + } + }, + { + "deliveryStart": "2025-10-02T11:30:00Z", + "deliveryEnd": "2025-10-02T11:45:00Z", + "entryPerArea": { + "SE3": 597.03, + "SE4": 672.83 + } + }, + { + "deliveryStart": "2025-10-02T11:45:00Z", + "deliveryEnd": "2025-10-02T12:00:00Z", + "entryPerArea": { + "SE3": 608.61, + "SE4": 685.19 + } + }, + { + "deliveryStart": "2025-10-02T12:00:00Z", + "deliveryEnd": "2025-10-02T12:15:00Z", + "entryPerArea": { + "SE3": 599.24, + "SE4": 676.91 + } + }, + { + "deliveryStart": "2025-10-02T12:15:00Z", + "deliveryEnd": "2025-10-02T12:30:00Z", + "entryPerArea": { + "SE3": 649.77, + "SE4": 729.54 + } + }, + { + "deliveryStart": "2025-10-02T12:30:00Z", + "deliveryEnd": "2025-10-02T12:45:00Z", + "entryPerArea": { + "SE3": 728.22, + "SE4": 821.23 + } + }, + { + "deliveryStart": "2025-10-02T12:45:00Z", + "deliveryEnd": "2025-10-02T13:00:00Z", + "entryPerArea": { + "SE3": 803.91, + "SE4": 909.06 + } + }, + { + "deliveryStart": "2025-10-02T13:00:00Z", + "deliveryEnd": "2025-10-02T13:15:00Z", + "entryPerArea": { + "SE3": 594.38, + "SE4": 679.23 + } + }, + { + "deliveryStart": "2025-10-02T13:15:00Z", + "deliveryEnd": "2025-10-02T13:30:00Z", + "entryPerArea": { + "SE3": 738.48, + "SE4": 825.09 + } + }, + { + "deliveryStart": "2025-10-02T13:30:00Z", + "deliveryEnd": "2025-10-02T13:45:00Z", + "entryPerArea": { + "SE3": 873.53, + "SE4": 962.02 + } + }, + { + "deliveryStart": "2025-10-02T13:45:00Z", + "deliveryEnd": "2025-10-02T14:00:00Z", + "entryPerArea": { + "SE3": 994.57, + "SE4": 1083.5 + } + }, + { + "deliveryStart": "2025-10-02T14:00:00Z", + "deliveryEnd": "2025-10-02T14:15:00Z", + "entryPerArea": { + "SE3": 733.52, + "SE4": 813.18 + } + }, + { + "deliveryStart": "2025-10-02T14:15:00Z", + "deliveryEnd": "2025-10-02T14:30:00Z", + "entryPerArea": { + "SE3": 864.59, + "SE4": 944.04 + } + }, + { + "deliveryStart": "2025-10-02T14:30:00Z", + "deliveryEnd": "2025-10-02T14:45:00Z", + "entryPerArea": { + "SE3": 1032.08, + "SE4": 1113.18 + } + }, + { + "deliveryStart": "2025-10-02T14:45:00Z", + "deliveryEnd": "2025-10-02T15:00:00Z", + "entryPerArea": { + "SE3": 1153.01, + "SE4": 1241.61 + } + }, + { + "deliveryStart": "2025-10-02T15:00:00Z", + "deliveryEnd": "2025-10-02T15:15:00Z", + "entryPerArea": { + "SE3": 1271.18, + "SE4": 1017.41 + } + }, + { + "deliveryStart": "2025-10-02T15:15:00Z", + "deliveryEnd": "2025-10-02T15:30:00Z", + "entryPerArea": { + "SE3": 1375.23, + "SE4": 1093.1 + } + }, + { + "deliveryStart": "2025-10-02T15:30:00Z", + "deliveryEnd": "2025-10-02T15:45:00Z", + "entryPerArea": { + "SE3": 1544.82, + "SE4": 1244.81 + } + }, + { + "deliveryStart": "2025-10-02T15:45:00Z", + "deliveryEnd": "2025-10-02T16:00:00Z", + "entryPerArea": { + "SE3": 2412.17, + "SE4": 1960.12 + } + }, + { + "deliveryStart": "2025-10-02T16:00:00Z", + "deliveryEnd": "2025-10-02T16:15:00Z", + "entryPerArea": { + "SE3": 1677.66, + "SE4": 1334.3 + } + }, + { + "deliveryStart": "2025-10-02T16:15:00Z", + "deliveryEnd": "2025-10-02T16:30:00Z", + "entryPerArea": { + "SE3": 2010.55, + "SE4": 1606.61 + } + }, + { + "deliveryStart": "2025-10-02T16:30:00Z", + "deliveryEnd": "2025-10-02T16:45:00Z", + "entryPerArea": { + "SE3": 2524.38, + "SE4": 2013.53 + } + }, + { + "deliveryStart": "2025-10-02T16:45:00Z", + "deliveryEnd": "2025-10-02T17:00:00Z", + "entryPerArea": { + "SE3": 3288.35, + "SE4": 2617.73 + } + }, + { + "deliveryStart": "2025-10-02T17:00:00Z", + "deliveryEnd": "2025-10-02T17:15:00Z", + "entryPerArea": { + "SE3": 3065.69, + "SE4": 2472.19 + } + }, + { + "deliveryStart": "2025-10-02T17:15:00Z", + "deliveryEnd": "2025-10-02T17:30:00Z", + "entryPerArea": { + "SE3": 2824.72, + "SE4": 2276.46 + } + }, + { + "deliveryStart": "2025-10-02T17:30:00Z", + "deliveryEnd": "2025-10-02T17:45:00Z", + "entryPerArea": { + "SE3": 2279.66, + "SE4": 1835.44 + } + }, + { + "deliveryStart": "2025-10-02T17:45:00Z", + "deliveryEnd": "2025-10-02T18:00:00Z", + "entryPerArea": { + "SE3": 1723.78, + "SE4": 1385.38 + } + }, + { + "deliveryStart": "2025-10-02T18:00:00Z", + "deliveryEnd": "2025-10-02T18:15:00Z", + "entryPerArea": { + "SE3": 1935.08, + "SE4": 1532.57 + } + }, + { + "deliveryStart": "2025-10-02T18:15:00Z", + "deliveryEnd": "2025-10-02T18:30:00Z", + "entryPerArea": { + "SE3": 1568.54, + "SE4": 1240.18 + } + }, + { + "deliveryStart": "2025-10-02T18:30:00Z", + "deliveryEnd": "2025-10-02T18:45:00Z", + "entryPerArea": { + "SE3": 1430.51, + "SE4": 1115.61 + } + }, + { + "deliveryStart": "2025-10-02T18:45:00Z", + "deliveryEnd": "2025-10-02T19:00:00Z", + "entryPerArea": { + "SE3": 1377.66, + "SE4": 1075.12 + } + }, + { + "deliveryStart": "2025-10-02T19:00:00Z", + "deliveryEnd": "2025-10-02T19:15:00Z", + "entryPerArea": { + "SE3": 1408.44, + "SE4": 1108.66 + } + }, + { + "deliveryStart": "2025-10-02T19:15:00Z", + "deliveryEnd": "2025-10-02T19:30:00Z", + "entryPerArea": { + "SE3": 1326.79, + "SE4": 1049.74 + } + }, + { + "deliveryStart": "2025-10-02T19:30:00Z", + "deliveryEnd": "2025-10-02T19:45:00Z", + "entryPerArea": { + "SE3": 1210.94, + "SE4": 951.1 + } + }, + { + "deliveryStart": "2025-10-02T19:45:00Z", + "deliveryEnd": "2025-10-02T20:00:00Z", + "entryPerArea": { + "SE3": 1293.58, + "SE4": 1026.79 + } + }, + { + "deliveryStart": "2025-10-02T20:00:00Z", + "deliveryEnd": "2025-10-02T20:15:00Z", + "entryPerArea": { + "SE3": 1385.71, + "SE4": 1091.0 + } + }, + { + "deliveryStart": "2025-10-02T20:15:00Z", + "deliveryEnd": "2025-10-02T20:30:00Z", + "entryPerArea": { + "SE3": 1341.47, + "SE4": 1104.13 + } + }, + { + "deliveryStart": "2025-10-02T20:30:00Z", + "deliveryEnd": "2025-10-02T20:45:00Z", + "entryPerArea": { + "SE3": 1284.98, + "SE4": 1024.36 + } + }, + { + "deliveryStart": "2025-10-02T20:45:00Z", + "deliveryEnd": "2025-10-02T21:00:00Z", + "entryPerArea": { + "SE3": 1071.47, + "SE4": 892.51 + } + }, + { + "deliveryStart": "2025-10-02T21:00:00Z", + "deliveryEnd": "2025-10-02T21:15:00Z", + "entryPerArea": { + "SE3": 1218.0, + "SE4": 1123.99 + } + }, + { + "deliveryStart": "2025-10-02T21:15:00Z", + "deliveryEnd": "2025-10-02T21:30:00Z", + "entryPerArea": { + "SE3": 1112.3, + "SE4": 1001.63 + } + }, + { + "deliveryStart": "2025-10-02T21:30:00Z", + "deliveryEnd": "2025-10-02T21:45:00Z", + "entryPerArea": { + "SE3": 873.64, + "SE4": 806.67 + } + }, + { + "deliveryStart": "2025-10-02T21:45:00Z", + "deliveryEnd": "2025-10-02T22:00:00Z", + "entryPerArea": { + "SE3": 646.9, + "SE4": 591.84 } } ], "blockPriceAggregates": [ { "blockName": "Off-peak 1", - "deliveryStart": "2024-11-05T23:00:00Z", - "deliveryEnd": "2024-11-06T07:00:00Z", + "deliveryStart": "2025-10-01T22:00:00Z", + "deliveryEnd": "2025-10-02T06:00:00Z", "averagePricePerArea": { "SE3": { - "average": 422.51, - "min": 74.06, - "max": 1820.5 + "average": 961.76, + "min": 673.05, + "max": 1831.25 }, "SE4": { - "average": 706.61, - "min": 157.34, - "max": 2449.96 + "average": 1102.25, + "min": 758.78, + "max": 2182.34 } } }, { "blockName": "Peak", - "deliveryStart": "2024-11-06T07:00:00Z", - "deliveryEnd": "2024-11-06T19:00:00Z", + "deliveryStart": "2025-10-02T06:00:00Z", + "deliveryEnd": "2025-10-02T18:00:00Z", "averagePricePerArea": { "SE3": { - "average": 1346.82, - "min": 903.31, - "max": 2366.57 + "average": 1191.34, + "min": 563.38, + "max": 3288.35 }, "SE4": { - "average": 2306.88, - "min": 1362.84, - "max": 5511.77 + "average": 1155.07, + "min": 635.76, + "max": 2617.73 } } }, { "blockName": "Off-peak 2", - "deliveryStart": "2024-11-06T19:00:00Z", - "deliveryEnd": "2024-11-06T23:00:00Z", + "deliveryStart": "2025-10-02T18:00:00Z", + "deliveryEnd": "2025-10-02T22:00:00Z", "averagePricePerArea": { "SE3": { - "average": 518.43, - "min": 250.64, - "max": 716.82 + "average": 1280.38, + "min": 646.9, + "max": 1935.08 }, "SE4": { - "average": 1153.25, - "min": 539.42, - "max": 1624.33 + "average": 1045.99, + "min": 591.84, + "max": 1532.57 } } } ], "currency": "SEK", - "exchangeRate": 11.66314, + "exchangeRate": 11.03362, "areaStates": [ { "state": "Final", @@ -262,11 +838,11 @@ "areaAverages": [ { "areaCode": "SE3", - "price": 900.65 + "price": 1129.65 }, { "areaCode": "SE4", - "price": 1581.19 + "price": 1119.28 } ] } diff --git a/tests/components/nordpool/fixtures/delivery_period_yesterday.json b/tests/components/nordpool/fixtures/delivery_period_yesterday.json index bc79aeb99f07ba..16af0a569345a2 100644 --- a/tests/components/nordpool/fixtures/delivery_period_yesterday.json +++ b/tests/components/nordpool/fixtures/delivery_period_yesterday.json @@ -1,258 +1,258 @@ { - "deliveryDateCET": "2024-11-04", + "deliveryDateCET": "2025-09-30", "version": 3, - "updatedAt": "2024-11-04T08:09:11.1931991Z", + "updatedAt": "2025-09-29T11:17:12.3019385Z", "deliveryAreas": ["SE3", "SE4"], "market": "DayAhead", "multiAreaEntries": [ { - "deliveryStart": "2024-11-03T23:00:00Z", - "deliveryEnd": "2024-11-04T00:00:00Z", + "deliveryStart": "2025-09-29T22:00:00Z", + "deliveryEnd": "2025-09-29T23:00:00Z", "entryPerArea": { - "SE3": 66.13, - "SE4": 78.59 + "SE3": 278.63, + "SE4": 354.65 } }, { - "deliveryStart": "2024-11-04T00:00:00Z", - "deliveryEnd": "2024-11-04T01:00:00Z", + "deliveryStart": "2025-09-29T23:00:00Z", + "deliveryEnd": "2025-09-30T00:00:00Z", "entryPerArea": { - "SE3": 72.54, - "SE4": 86.51 + "SE3": 261.85, + "SE4": 336.89 } }, { - "deliveryStart": "2024-11-04T01:00:00Z", - "deliveryEnd": "2024-11-04T02:00:00Z", + "deliveryStart": "2025-09-30T00:00:00Z", + "deliveryEnd": "2025-09-30T01:00:00Z", "entryPerArea": { - "SE3": 73.12, - "SE4": 84.88 + "SE3": 242.43, + "SE4": 313.16 } }, { - "deliveryStart": "2024-11-04T02:00:00Z", - "deliveryEnd": "2024-11-04T03:00:00Z", + "deliveryStart": "2025-09-30T01:00:00Z", + "deliveryEnd": "2025-09-30T02:00:00Z", "entryPerArea": { - "SE3": 171.97, - "SE4": 217.26 + "SE3": 322.65, + "SE4": 401.0 } }, { - "deliveryStart": "2024-11-04T03:00:00Z", - "deliveryEnd": "2024-11-04T04:00:00Z", + "deliveryStart": "2025-09-30T02:00:00Z", + "deliveryEnd": "2025-09-30T03:00:00Z", "entryPerArea": { - "SE3": 181.05, - "SE4": 227.74 + "SE3": 243.2, + "SE4": 311.51 } }, { - "deliveryStart": "2024-11-04T04:00:00Z", - "deliveryEnd": "2024-11-04T05:00:00Z", + "deliveryStart": "2025-09-30T03:00:00Z", + "deliveryEnd": "2025-09-30T04:00:00Z", "entryPerArea": { - "SE3": 360.71, - "SE4": 414.61 + "SE3": 596.53, + "SE4": 695.52 } }, { - "deliveryStart": "2024-11-04T05:00:00Z", - "deliveryEnd": "2024-11-04T06:00:00Z", + "deliveryStart": "2025-09-30T04:00:00Z", + "deliveryEnd": "2025-09-30T05:00:00Z", "entryPerArea": { - "SE3": 917.83, - "SE4": 1439.33 + "SE3": 899.77, + "SE4": 1047.52 } }, { - "deliveryStart": "2024-11-04T06:00:00Z", - "deliveryEnd": "2024-11-04T07:00:00Z", + "deliveryStart": "2025-09-30T05:00:00Z", + "deliveryEnd": "2025-09-30T06:00:00Z", "entryPerArea": { - "SE3": 1426.17, - "SE4": 1695.95 + "SE3": 1909.0, + "SE4": 2247.98 } }, { - "deliveryStart": "2024-11-04T07:00:00Z", - "deliveryEnd": "2024-11-04T08:00:00Z", + "deliveryStart": "2025-09-30T06:00:00Z", + "deliveryEnd": "2025-09-30T07:00:00Z", "entryPerArea": { - "SE3": 1350.96, - "SE4": 1605.13 + "SE3": 1432.52, + "SE4": 1681.24 } }, { - "deliveryStart": "2024-11-04T08:00:00Z", - "deliveryEnd": "2024-11-04T09:00:00Z", + "deliveryStart": "2025-09-30T07:00:00Z", + "deliveryEnd": "2025-09-30T08:00:00Z", "entryPerArea": { - "SE3": 1195.06, - "SE4": 1393.46 + "SE3": 1127.52, + "SE4": 1304.96 } }, { - "deliveryStart": "2024-11-04T09:00:00Z", - "deliveryEnd": "2024-11-04T10:00:00Z", + "deliveryStart": "2025-09-30T08:00:00Z", + "deliveryEnd": "2025-09-30T09:00:00Z", "entryPerArea": { - "SE3": 992.35, - "SE4": 1126.71 + "SE3": 966.75, + "SE4": 1073.34 } }, { - "deliveryStart": "2024-11-04T10:00:00Z", - "deliveryEnd": "2024-11-04T11:00:00Z", + "deliveryStart": "2025-09-30T09:00:00Z", + "deliveryEnd": "2025-09-30T10:00:00Z", "entryPerArea": { - "SE3": 976.63, - "SE4": 1107.97 + "SE3": 882.55, + "SE4": 1003.93 } }, { - "deliveryStart": "2024-11-04T11:00:00Z", - "deliveryEnd": "2024-11-04T12:00:00Z", + "deliveryStart": "2025-09-30T10:00:00Z", + "deliveryEnd": "2025-09-30T11:00:00Z", "entryPerArea": { - "SE3": 952.76, - "SE4": 1085.73 + "SE3": 841.72, + "SE4": 947.44 } }, { - "deliveryStart": "2024-11-04T12:00:00Z", - "deliveryEnd": "2024-11-04T13:00:00Z", + "deliveryStart": "2025-09-30T11:00:00Z", + "deliveryEnd": "2025-09-30T12:00:00Z", "entryPerArea": { - "SE3": 1029.37, - "SE4": 1177.71 + "SE3": 821.53, + "SE4": 927.24 } }, { - "deliveryStart": "2024-11-04T13:00:00Z", - "deliveryEnd": "2024-11-04T14:00:00Z", + "deliveryStart": "2025-09-30T12:00:00Z", + "deliveryEnd": "2025-09-30T13:00:00Z", "entryPerArea": { - "SE3": 1043.35, - "SE4": 1194.59 + "SE3": 864.35, + "SE4": 970.5 } }, { - "deliveryStart": "2024-11-04T14:00:00Z", - "deliveryEnd": "2024-11-04T15:00:00Z", + "deliveryStart": "2025-09-30T13:00:00Z", + "deliveryEnd": "2025-09-30T14:00:00Z", "entryPerArea": { - "SE3": 1359.57, - "SE4": 1561.12 + "SE3": 931.88, + "SE4": 1046.64 } }, { - "deliveryStart": "2024-11-04T15:00:00Z", - "deliveryEnd": "2024-11-04T16:00:00Z", + "deliveryStart": "2025-09-30T14:00:00Z", + "deliveryEnd": "2025-09-30T15:00:00Z", "entryPerArea": { - "SE3": 1848.35, - "SE4": 2145.84 + "SE3": 1039.13, + "SE4": 1165.04 } }, { - "deliveryStart": "2024-11-04T16:00:00Z", - "deliveryEnd": "2024-11-04T17:00:00Z", + "deliveryStart": "2025-09-30T15:00:00Z", + "deliveryEnd": "2025-09-30T16:00:00Z", "entryPerArea": { - "SE3": 2812.53, - "SE4": 3313.53 + "SE3": 1296.57, + "SE4": 1520.91 } }, { - "deliveryStart": "2024-11-04T17:00:00Z", - "deliveryEnd": "2024-11-04T18:00:00Z", + "deliveryStart": "2025-09-30T16:00:00Z", + "deliveryEnd": "2025-09-30T17:00:00Z", "entryPerArea": { - "SE3": 2351.69, - "SE4": 2751.87 + "SE3": 2652.18, + "SE4": 3083.2 } }, { - "deliveryStart": "2024-11-04T18:00:00Z", - "deliveryEnd": "2024-11-04T19:00:00Z", + "deliveryStart": "2025-09-30T17:00:00Z", + "deliveryEnd": "2025-09-30T18:00:00Z", "entryPerArea": { - "SE3": 1553.08, - "SE4": 1842.77 + "SE3": 2135.98, + "SE4": 2552.32 } }, { - "deliveryStart": "2024-11-04T19:00:00Z", - "deliveryEnd": "2024-11-04T20:00:00Z", + "deliveryStart": "2025-09-30T18:00:00Z", + "deliveryEnd": "2025-09-30T19:00:00Z", "entryPerArea": { - "SE3": 1165.02, - "SE4": 1398.35 + "SE3": 1109.76, + "SE4": 1305.73 } }, { - "deliveryStart": "2024-11-04T20:00:00Z", - "deliveryEnd": "2024-11-04T21:00:00Z", + "deliveryStart": "2025-09-30T19:00:00Z", + "deliveryEnd": "2025-09-30T20:00:00Z", "entryPerArea": { - "SE3": 1007.48, - "SE4": 1172.35 + "SE3": 973.81, + "SE4": 1130.83 } }, { - "deliveryStart": "2024-11-04T21:00:00Z", - "deliveryEnd": "2024-11-04T22:00:00Z", + "deliveryStart": "2025-09-30T20:00:00Z", + "deliveryEnd": "2025-09-30T21:00:00Z", "entryPerArea": { - "SE3": 792.09, - "SE4": 920.28 + "SE3": 872.18, + "SE4": 1019.05 } }, { - "deliveryStart": "2024-11-04T22:00:00Z", - "deliveryEnd": "2024-11-04T23:00:00Z", + "deliveryStart": "2025-09-30T21:00:00Z", + "deliveryEnd": "2025-09-30T22:00:00Z", "entryPerArea": { - "SE3": 465.38, - "SE4": 528.83 + "SE3": 697.17, + "SE4": 812.37 } } ], "blockPriceAggregates": [ { "blockName": "Off-peak 1", - "deliveryStart": "2024-11-03T23:00:00Z", - "deliveryEnd": "2024-11-04T07:00:00Z", + "deliveryStart": "2025-09-29T22:00:00Z", + "deliveryEnd": "2025-09-30T06:00:00Z", "averagePricePerArea": { "SE3": { - "average": 408.69, - "min": 66.13, - "max": 1426.17 + "average": 594.26, + "min": 242.43, + "max": 1909.0 }, "SE4": { - "average": 530.61, - "min": 78.59, - "max": 1695.95 + "average": 713.53, + "min": 311.51, + "max": 2247.98 } } }, { "blockName": "Peak", - "deliveryStart": "2024-11-04T07:00:00Z", - "deliveryEnd": "2024-11-04T19:00:00Z", + "deliveryStart": "2025-09-30T06:00:00Z", + "deliveryEnd": "2025-09-30T18:00:00Z", "averagePricePerArea": { "SE3": { - "average": 1455.48, - "min": 952.76, - "max": 2812.53 + "average": 1249.39, + "min": 821.53, + "max": 2652.18 }, "SE4": { - "average": 1692.2, - "min": 1085.73, - "max": 3313.53 + "average": 1439.73, + "min": 927.24, + "max": 3083.2 } } }, { "blockName": "Off-peak 2", - "deliveryStart": "2024-11-04T19:00:00Z", - "deliveryEnd": "2024-11-04T23:00:00Z", + "deliveryStart": "2025-09-30T18:00:00Z", + "deliveryEnd": "2025-09-30T22:00:00Z", "averagePricePerArea": { "SE3": { - "average": 857.49, - "min": 465.38, - "max": 1165.02 + "average": 913.23, + "min": 697.17, + "max": 1109.76 }, "SE4": { - "average": 1004.95, - "min": 528.83, - "max": 1398.35 + "average": 1067.0, + "min": 812.37, + "max": 1305.73 } } } ], "currency": "SEK", - "exchangeRate": 11.64318, + "exchangeRate": 11.03467, "areaStates": [ { "state": "Final", @@ -262,11 +262,11 @@ "areaAverages": [ { "areaCode": "SE3", - "price": 1006.88 + "price": 974.99 }, { "areaCode": "SE4", - "price": 1190.46 + "price": 1135.54 } ] } diff --git a/tests/components/nordpool/fixtures/indices_15.json b/tests/components/nordpool/fixtures/indices_15.json index 63af9840098bca..0af23104104b80 100644 --- a/tests/components/nordpool/fixtures/indices_15.json +++ b/tests/components/nordpool/fixtures/indices_15.json @@ -1,688 +1,688 @@ { - "deliveryDateCET": "2025-07-06", - "version": 2, - "updatedAt": "2025-07-05T10:56:42.3755929Z", + "deliveryDateCET": "2025-10-01", + "version": 3, + "updatedAt": "2025-09-30T12:08:18.4894194Z", "market": "DayAhead", "indexNames": ["SE3"], "currency": "SEK", "resolutionInMinutes": 15, "areaStates": [ { - "state": "Preliminary", + "state": "Final", "areas": ["SE3"] } ], "multiIndexEntries": [ { - "deliveryStart": "2025-07-05T22:00:00Z", - "deliveryEnd": "2025-07-05T22:15:00Z", + "deliveryStart": "2025-09-30T22:00:00Z", + "deliveryEnd": "2025-09-30T22:15:00Z", "entryPerArea": { - "SE3": 43.57 + "SE3": 556.68 } }, { - "deliveryStart": "2025-07-05T22:15:00Z", - "deliveryEnd": "2025-07-05T22:30:00Z", + "deliveryStart": "2025-09-30T22:15:00Z", + "deliveryEnd": "2025-09-30T22:30:00Z", "entryPerArea": { - "SE3": 43.57 + "SE3": 519.88 } }, { - "deliveryStart": "2025-07-05T22:30:00Z", - "deliveryEnd": "2025-07-05T22:45:00Z", + "deliveryStart": "2025-09-30T22:30:00Z", + "deliveryEnd": "2025-09-30T22:45:00Z", "entryPerArea": { - "SE3": 43.57 + "SE3": 508.28 } }, { - "deliveryStart": "2025-07-05T22:45:00Z", - "deliveryEnd": "2025-07-05T23:00:00Z", + "deliveryStart": "2025-09-30T22:45:00Z", + "deliveryEnd": "2025-09-30T23:00:00Z", "entryPerArea": { - "SE3": 43.57 + "SE3": 509.93 } }, { - "deliveryStart": "2025-07-05T23:00:00Z", - "deliveryEnd": "2025-07-05T23:15:00Z", + "deliveryStart": "2025-09-30T23:00:00Z", + "deliveryEnd": "2025-09-30T23:15:00Z", "entryPerArea": { - "SE3": 36.47 + "SE3": 501.64 } }, { - "deliveryStart": "2025-07-05T23:15:00Z", - "deliveryEnd": "2025-07-05T23:30:00Z", + "deliveryStart": "2025-09-30T23:15:00Z", + "deliveryEnd": "2025-09-30T23:30:00Z", "entryPerArea": { - "SE3": 36.47 + "SE3": 509.05 } }, { - "deliveryStart": "2025-07-05T23:30:00Z", - "deliveryEnd": "2025-07-05T23:45:00Z", + "deliveryStart": "2025-09-30T23:30:00Z", + "deliveryEnd": "2025-09-30T23:45:00Z", "entryPerArea": { - "SE3": 36.47 + "SE3": 491.03 } }, { - "deliveryStart": "2025-07-05T23:45:00Z", - "deliveryEnd": "2025-07-06T00:00:00Z", + "deliveryStart": "2025-09-30T23:45:00Z", + "deliveryEnd": "2025-10-01T00:00:00Z", "entryPerArea": { - "SE3": 36.47 + "SE3": 442.07 } }, { - "deliveryStart": "2025-07-06T00:00:00Z", - "deliveryEnd": "2025-07-06T00:15:00Z", + "deliveryStart": "2025-10-01T00:00:00Z", + "deliveryEnd": "2025-10-01T00:15:00Z", "entryPerArea": { - "SE3": 35.57 + "SE3": 504.08 } }, { - "deliveryStart": "2025-07-06T00:15:00Z", - "deliveryEnd": "2025-07-06T00:30:00Z", + "deliveryStart": "2025-10-01T00:15:00Z", + "deliveryEnd": "2025-10-01T00:30:00Z", "entryPerArea": { - "SE3": 35.57 + "SE3": 504.85 } }, { - "deliveryStart": "2025-07-06T00:30:00Z", - "deliveryEnd": "2025-07-06T00:45:00Z", + "deliveryStart": "2025-10-01T00:30:00Z", + "deliveryEnd": "2025-10-01T00:45:00Z", "entryPerArea": { - "SE3": 35.57 + "SE3": 504.3 } }, { - "deliveryStart": "2025-07-06T00:45:00Z", - "deliveryEnd": "2025-07-06T01:00:00Z", + "deliveryStart": "2025-10-01T00:45:00Z", + "deliveryEnd": "2025-10-01T01:00:00Z", "entryPerArea": { - "SE3": 35.57 + "SE3": 506.29 } }, { - "deliveryStart": "2025-07-06T01:00:00Z", - "deliveryEnd": "2025-07-06T01:15:00Z", + "deliveryStart": "2025-10-01T01:00:00Z", + "deliveryEnd": "2025-10-01T01:15:00Z", "entryPerArea": { - "SE3": 30.73 + "SE3": 442.07 } }, { - "deliveryStart": "2025-07-06T01:15:00Z", - "deliveryEnd": "2025-07-06T01:30:00Z", + "deliveryStart": "2025-10-01T01:15:00Z", + "deliveryEnd": "2025-10-01T01:30:00Z", "entryPerArea": { - "SE3": 30.73 + "SE3": 441.96 } }, { - "deliveryStart": "2025-07-06T01:30:00Z", - "deliveryEnd": "2025-07-06T01:45:00Z", + "deliveryStart": "2025-10-01T01:30:00Z", + "deliveryEnd": "2025-10-01T01:45:00Z", "entryPerArea": { - "SE3": 30.73 + "SE3": 442.07 } }, { - "deliveryStart": "2025-07-06T01:45:00Z", - "deliveryEnd": "2025-07-06T02:00:00Z", + "deliveryStart": "2025-10-01T01:45:00Z", + "deliveryEnd": "2025-10-01T02:00:00Z", "entryPerArea": { - "SE3": 30.73 + "SE3": 442.07 } }, { - "deliveryStart": "2025-07-06T02:00:00Z", - "deliveryEnd": "2025-07-06T02:15:00Z", + "deliveryStart": "2025-10-01T02:00:00Z", + "deliveryEnd": "2025-10-01T02:15:00Z", "entryPerArea": { - "SE3": 32.42 + "SE3": 441.96 } }, { - "deliveryStart": "2025-07-06T02:15:00Z", - "deliveryEnd": "2025-07-06T02:30:00Z", + "deliveryStart": "2025-10-01T02:15:00Z", + "deliveryEnd": "2025-10-01T02:30:00Z", "entryPerArea": { - "SE3": 32.42 + "SE3": 483.3 } }, { - "deliveryStart": "2025-07-06T02:30:00Z", - "deliveryEnd": "2025-07-06T02:45:00Z", + "deliveryStart": "2025-10-01T02:30:00Z", + "deliveryEnd": "2025-10-01T02:45:00Z", "entryPerArea": { - "SE3": 32.42 + "SE3": 484.29 } }, { - "deliveryStart": "2025-07-06T02:45:00Z", - "deliveryEnd": "2025-07-06T03:00:00Z", + "deliveryStart": "2025-10-01T02:45:00Z", + "deliveryEnd": "2025-10-01T03:00:00Z", "entryPerArea": { - "SE3": 32.42 + "SE3": 574.7 } }, { - "deliveryStart": "2025-07-06T03:00:00Z", - "deliveryEnd": "2025-07-06T03:15:00Z", + "deliveryStart": "2025-10-01T03:00:00Z", + "deliveryEnd": "2025-10-01T03:15:00Z", "entryPerArea": { - "SE3": 38.73 + "SE3": 543.31 } }, { - "deliveryStart": "2025-07-06T03:15:00Z", - "deliveryEnd": "2025-07-06T03:30:00Z", + "deliveryStart": "2025-10-01T03:15:00Z", + "deliveryEnd": "2025-10-01T03:30:00Z", "entryPerArea": { - "SE3": 38.73 + "SE3": 578.01 } }, { - "deliveryStart": "2025-07-06T03:30:00Z", - "deliveryEnd": "2025-07-06T03:45:00Z", + "deliveryStart": "2025-10-01T03:30:00Z", + "deliveryEnd": "2025-10-01T03:45:00Z", "entryPerArea": { - "SE3": 38.73 + "SE3": 774.96 } }, { - "deliveryStart": "2025-07-06T03:45:00Z", - "deliveryEnd": "2025-07-06T04:00:00Z", + "deliveryStart": "2025-10-01T03:45:00Z", + "deliveryEnd": "2025-10-01T04:00:00Z", "entryPerArea": { - "SE3": 38.73 + "SE3": 787.0 } }, { - "deliveryStart": "2025-07-06T04:00:00Z", - "deliveryEnd": "2025-07-06T04:15:00Z", + "deliveryStart": "2025-10-01T04:00:00Z", + "deliveryEnd": "2025-10-01T04:15:00Z", "entryPerArea": { - "SE3": 42.78 + "SE3": 902.38 } }, { - "deliveryStart": "2025-07-06T04:15:00Z", - "deliveryEnd": "2025-07-06T04:30:00Z", + "deliveryStart": "2025-10-01T04:15:00Z", + "deliveryEnd": "2025-10-01T04:30:00Z", "entryPerArea": { - "SE3": 42.78 + "SE3": 1079.32 } }, { - "deliveryStart": "2025-07-06T04:30:00Z", - "deliveryEnd": "2025-07-06T04:45:00Z", + "deliveryStart": "2025-10-01T04:30:00Z", + "deliveryEnd": "2025-10-01T04:45:00Z", "entryPerArea": { - "SE3": 42.78 + "SE3": 1222.67 } }, { - "deliveryStart": "2025-07-06T04:45:00Z", - "deliveryEnd": "2025-07-06T05:00:00Z", + "deliveryStart": "2025-10-01T04:45:00Z", + "deliveryEnd": "2025-10-01T05:00:00Z", "entryPerArea": { - "SE3": 42.78 + "SE3": 1394.63 } }, { - "deliveryStart": "2025-07-06T05:00:00Z", - "deliveryEnd": "2025-07-06T05:15:00Z", + "deliveryStart": "2025-10-01T05:00:00Z", + "deliveryEnd": "2025-10-01T05:15:00Z", "entryPerArea": { - "SE3": 54.71 + "SE3": 1529.36 } }, { - "deliveryStart": "2025-07-06T05:15:00Z", - "deliveryEnd": "2025-07-06T05:30:00Z", + "deliveryStart": "2025-10-01T05:15:00Z", + "deliveryEnd": "2025-10-01T05:30:00Z", "entryPerArea": { - "SE3": 54.71 + "SE3": 1724.53 } }, { - "deliveryStart": "2025-07-06T05:30:00Z", - "deliveryEnd": "2025-07-06T05:45:00Z", + "deliveryStart": "2025-10-01T05:30:00Z", + "deliveryEnd": "2025-10-01T05:45:00Z", "entryPerArea": { - "SE3": 54.71 + "SE3": 1809.96 } }, { - "deliveryStart": "2025-07-06T05:45:00Z", - "deliveryEnd": "2025-07-06T06:00:00Z", + "deliveryStart": "2025-10-01T05:45:00Z", + "deliveryEnd": "2025-10-01T06:00:00Z", "entryPerArea": { - "SE3": 54.71 + "SE3": 1713.04 } }, { - "deliveryStart": "2025-07-06T06:00:00Z", - "deliveryEnd": "2025-07-06T06:15:00Z", + "deliveryStart": "2025-10-01T06:00:00Z", + "deliveryEnd": "2025-10-01T06:15:00Z", "entryPerArea": { - "SE3": 83.87 + "SE3": 1925.9 } }, { - "deliveryStart": "2025-07-06T06:15:00Z", - "deliveryEnd": "2025-07-06T06:30:00Z", + "deliveryStart": "2025-10-01T06:15:00Z", + "deliveryEnd": "2025-10-01T06:30:00Z", "entryPerArea": { - "SE3": 83.87 + "SE3": 1440.06 } }, { - "deliveryStart": "2025-07-06T06:30:00Z", - "deliveryEnd": "2025-07-06T06:45:00Z", + "deliveryStart": "2025-10-01T06:30:00Z", + "deliveryEnd": "2025-10-01T06:45:00Z", "entryPerArea": { - "SE3": 83.87 + "SE3": 1183.32 } }, { - "deliveryStart": "2025-07-06T06:45:00Z", - "deliveryEnd": "2025-07-06T07:00:00Z", + "deliveryStart": "2025-10-01T06:45:00Z", + "deliveryEnd": "2025-10-01T07:00:00Z", "entryPerArea": { - "SE3": 83.87 + "SE3": 962.95 } }, { - "deliveryStart": "2025-07-06T07:00:00Z", - "deliveryEnd": "2025-07-06T07:15:00Z", + "deliveryStart": "2025-10-01T07:00:00Z", + "deliveryEnd": "2025-10-01T07:15:00Z", "entryPerArea": { - "SE3": 78.8 + "SE3": 1402.04 } }, { - "deliveryStart": "2025-07-06T07:15:00Z", - "deliveryEnd": "2025-07-06T07:30:00Z", + "deliveryStart": "2025-10-01T07:15:00Z", + "deliveryEnd": "2025-10-01T07:30:00Z", "entryPerArea": { - "SE3": 78.8 + "SE3": 1060.65 } }, { - "deliveryStart": "2025-07-06T07:30:00Z", - "deliveryEnd": "2025-07-06T07:45:00Z", + "deliveryStart": "2025-10-01T07:30:00Z", + "deliveryEnd": "2025-10-01T07:45:00Z", "entryPerArea": { - "SE3": 78.8 + "SE3": 949.13 } }, { - "deliveryStart": "2025-07-06T07:45:00Z", - "deliveryEnd": "2025-07-06T08:00:00Z", + "deliveryStart": "2025-10-01T07:45:00Z", + "deliveryEnd": "2025-10-01T08:00:00Z", "entryPerArea": { - "SE3": 78.8 + "SE3": 841.82 } }, { - "deliveryStart": "2025-07-06T08:00:00Z", - "deliveryEnd": "2025-07-06T08:15:00Z", + "deliveryStart": "2025-10-01T08:00:00Z", + "deliveryEnd": "2025-10-01T08:15:00Z", "entryPerArea": { - "SE3": 92.09 + "SE3": 1037.44 } }, { - "deliveryStart": "2025-07-06T08:15:00Z", - "deliveryEnd": "2025-07-06T08:30:00Z", + "deliveryStart": "2025-10-01T08:15:00Z", + "deliveryEnd": "2025-10-01T08:30:00Z", "entryPerArea": { - "SE3": 92.09 + "SE3": 950.13 } }, { - "deliveryStart": "2025-07-06T08:30:00Z", - "deliveryEnd": "2025-07-06T08:45:00Z", + "deliveryStart": "2025-10-01T08:30:00Z", + "deliveryEnd": "2025-10-01T08:45:00Z", "entryPerArea": { - "SE3": 92.09 + "SE3": 826.13 } }, { - "deliveryStart": "2025-07-06T08:45:00Z", - "deliveryEnd": "2025-07-06T09:00:00Z", + "deliveryStart": "2025-10-01T08:45:00Z", + "deliveryEnd": "2025-10-01T09:00:00Z", "entryPerArea": { - "SE3": 92.09 + "SE3": 684.55 } }, { - "deliveryStart": "2025-07-06T09:00:00Z", - "deliveryEnd": "2025-07-06T09:15:00Z", + "deliveryStart": "2025-10-01T09:00:00Z", + "deliveryEnd": "2025-10-01T09:15:00Z", "entryPerArea": { - "SE3": 104.92 + "SE3": 861.6 } }, { - "deliveryStart": "2025-07-06T09:15:00Z", - "deliveryEnd": "2025-07-06T09:30:00Z", + "deliveryStart": "2025-10-01T09:15:00Z", + "deliveryEnd": "2025-10-01T09:30:00Z", "entryPerArea": { - "SE3": 104.92 + "SE3": 722.79 } }, { - "deliveryStart": "2025-07-06T09:30:00Z", - "deliveryEnd": "2025-07-06T09:45:00Z", + "deliveryStart": "2025-10-01T09:30:00Z", + "deliveryEnd": "2025-10-01T09:45:00Z", "entryPerArea": { - "SE3": 104.92 + "SE3": 640.57 } }, { - "deliveryStart": "2025-07-06T09:45:00Z", - "deliveryEnd": "2025-07-06T10:00:00Z", + "deliveryStart": "2025-10-01T09:45:00Z", + "deliveryEnd": "2025-10-01T10:00:00Z", "entryPerArea": { - "SE3": 104.92 + "SE3": 607.74 } }, { - "deliveryStart": "2025-07-06T10:00:00Z", - "deliveryEnd": "2025-07-06T10:15:00Z", + "deliveryStart": "2025-10-01T10:00:00Z", + "deliveryEnd": "2025-10-01T10:15:00Z", "entryPerArea": { - "SE3": 72.5 + "SE3": 674.05 } }, { - "deliveryStart": "2025-07-06T10:15:00Z", - "deliveryEnd": "2025-07-06T10:30:00Z", + "deliveryStart": "2025-10-01T10:15:00Z", + "deliveryEnd": "2025-10-01T10:30:00Z", "entryPerArea": { - "SE3": 72.5 + "SE3": 638.58 } }, { - "deliveryStart": "2025-07-06T10:30:00Z", - "deliveryEnd": "2025-07-06T10:45:00Z", + "deliveryStart": "2025-10-01T10:30:00Z", + "deliveryEnd": "2025-10-01T10:45:00Z", "entryPerArea": { - "SE3": 72.5 + "SE3": 638.47 } }, { - "deliveryStart": "2025-07-06T10:45:00Z", - "deliveryEnd": "2025-07-06T11:00:00Z", + "deliveryStart": "2025-10-01T10:45:00Z", + "deliveryEnd": "2025-10-01T11:00:00Z", "entryPerArea": { - "SE3": 72.5 + "SE3": 634.82 } }, { - "deliveryStart": "2025-07-06T11:00:00Z", - "deliveryEnd": "2025-07-06T11:15:00Z", + "deliveryStart": "2025-10-01T11:00:00Z", + "deliveryEnd": "2025-10-01T11:15:00Z", "entryPerArea": { - "SE3": 63.49 + "SE3": 637.36 } }, { - "deliveryStart": "2025-07-06T11:15:00Z", - "deliveryEnd": "2025-07-06T11:30:00Z", + "deliveryStart": "2025-10-01T11:15:00Z", + "deliveryEnd": "2025-10-01T11:30:00Z", "entryPerArea": { - "SE3": 63.49 + "SE3": 660.68 } }, { - "deliveryStart": "2025-07-06T11:30:00Z", - "deliveryEnd": "2025-07-06T11:45:00Z", + "deliveryStart": "2025-10-01T11:30:00Z", + "deliveryEnd": "2025-10-01T11:45:00Z", "entryPerArea": { - "SE3": 63.49 + "SE3": 679.14 } }, { - "deliveryStart": "2025-07-06T11:45:00Z", - "deliveryEnd": "2025-07-06T12:00:00Z", + "deliveryStart": "2025-10-01T11:45:00Z", + "deliveryEnd": "2025-10-01T12:00:00Z", "entryPerArea": { - "SE3": 63.49 + "SE3": 694.61 } }, { - "deliveryStart": "2025-07-06T12:00:00Z", - "deliveryEnd": "2025-07-06T12:15:00Z", + "deliveryStart": "2025-10-01T12:00:00Z", + "deliveryEnd": "2025-10-01T12:15:00Z", "entryPerArea": { - "SE3": 91.64 + "SE3": 622.33 } }, { - "deliveryStart": "2025-07-06T12:15:00Z", - "deliveryEnd": "2025-07-06T12:30:00Z", + "deliveryStart": "2025-10-01T12:15:00Z", + "deliveryEnd": "2025-10-01T12:30:00Z", "entryPerArea": { - "SE3": 91.64 + "SE3": 685.44 } }, { - "deliveryStart": "2025-07-06T12:30:00Z", - "deliveryEnd": "2025-07-06T12:45:00Z", + "deliveryStart": "2025-10-01T12:30:00Z", + "deliveryEnd": "2025-10-01T12:45:00Z", "entryPerArea": { - "SE3": 91.64 + "SE3": 732.85 } }, { - "deliveryStart": "2025-07-06T12:45:00Z", - "deliveryEnd": "2025-07-06T13:00:00Z", + "deliveryStart": "2025-10-01T12:45:00Z", + "deliveryEnd": "2025-10-01T13:00:00Z", "entryPerArea": { - "SE3": 91.64 + "SE3": 801.92 } }, { - "deliveryStart": "2025-07-06T13:00:00Z", - "deliveryEnd": "2025-07-06T13:15:00Z", + "deliveryStart": "2025-10-01T13:00:00Z", + "deliveryEnd": "2025-10-01T13:15:00Z", "entryPerArea": { - "SE3": 111.79 + "SE3": 629.4 } }, { - "deliveryStart": "2025-07-06T13:15:00Z", - "deliveryEnd": "2025-07-06T13:30:00Z", + "deliveryStart": "2025-10-01T13:15:00Z", + "deliveryEnd": "2025-10-01T13:30:00Z", "entryPerArea": { - "SE3": 111.79 + "SE3": 729.53 } }, { - "deliveryStart": "2025-07-06T13:30:00Z", - "deliveryEnd": "2025-07-06T13:45:00Z", + "deliveryStart": "2025-10-01T13:30:00Z", + "deliveryEnd": "2025-10-01T13:45:00Z", "entryPerArea": { - "SE3": 111.79 + "SE3": 884.81 } }, { - "deliveryStart": "2025-07-06T13:45:00Z", - "deliveryEnd": "2025-07-06T14:00:00Z", + "deliveryStart": "2025-10-01T13:45:00Z", + "deliveryEnd": "2025-10-01T14:00:00Z", "entryPerArea": { - "SE3": 111.79 + "SE3": 984.94 } }, { - "deliveryStart": "2025-07-06T14:00:00Z", - "deliveryEnd": "2025-07-06T14:15:00Z", + "deliveryStart": "2025-10-01T14:00:00Z", + "deliveryEnd": "2025-10-01T14:15:00Z", "entryPerArea": { - "SE3": 234.04 + "SE3": 615.26 } }, { - "deliveryStart": "2025-07-06T14:15:00Z", - "deliveryEnd": "2025-07-06T14:30:00Z", + "deliveryStart": "2025-10-01T14:15:00Z", + "deliveryEnd": "2025-10-01T14:30:00Z", "entryPerArea": { - "SE3": 234.04 + "SE3": 902.94 } }, { - "deliveryStart": "2025-07-06T14:30:00Z", - "deliveryEnd": "2025-07-06T14:45:00Z", + "deliveryStart": "2025-10-01T14:30:00Z", + "deliveryEnd": "2025-10-01T14:45:00Z", "entryPerArea": { - "SE3": 234.04 + "SE3": 1043.85 } }, { - "deliveryStart": "2025-07-06T14:45:00Z", - "deliveryEnd": "2025-07-06T15:00:00Z", + "deliveryStart": "2025-10-01T14:45:00Z", + "deliveryEnd": "2025-10-01T15:00:00Z", "entryPerArea": { - "SE3": 234.04 + "SE3": 1075.12 } }, { - "deliveryStart": "2025-07-06T15:00:00Z", - "deliveryEnd": "2025-07-06T15:15:00Z", + "deliveryStart": "2025-10-01T15:00:00Z", + "deliveryEnd": "2025-10-01T15:15:00Z", "entryPerArea": { - "SE3": 435.33 + "SE3": 980.52 } }, { - "deliveryStart": "2025-07-06T15:15:00Z", - "deliveryEnd": "2025-07-06T15:30:00Z", + "deliveryStart": "2025-10-01T15:15:00Z", + "deliveryEnd": "2025-10-01T15:30:00Z", "entryPerArea": { - "SE3": 435.33 + "SE3": 1162.66 } }, { - "deliveryStart": "2025-07-06T15:30:00Z", - "deliveryEnd": "2025-07-06T15:45:00Z", + "deliveryStart": "2025-10-01T15:30:00Z", + "deliveryEnd": "2025-10-01T15:45:00Z", "entryPerArea": { - "SE3": 435.33 + "SE3": 1453.87 } }, { - "deliveryStart": "2025-07-06T15:45:00Z", - "deliveryEnd": "2025-07-06T16:00:00Z", + "deliveryStart": "2025-10-01T15:45:00Z", + "deliveryEnd": "2025-10-01T16:00:00Z", "entryPerArea": { - "SE3": 435.33 + "SE3": 1955.96 } }, { - "deliveryStart": "2025-07-06T16:00:00Z", - "deliveryEnd": "2025-07-06T16:15:00Z", + "deliveryStart": "2025-10-01T16:00:00Z", + "deliveryEnd": "2025-10-01T16:15:00Z", "entryPerArea": { - "SE3": 431.84 + "SE3": 1423.48 } }, { - "deliveryStart": "2025-07-06T16:15:00Z", - "deliveryEnd": "2025-07-06T16:30:00Z", + "deliveryStart": "2025-10-01T16:15:00Z", + "deliveryEnd": "2025-10-01T16:30:00Z", "entryPerArea": { - "SE3": 431.84 + "SE3": 1900.04 } }, { - "deliveryStart": "2025-07-06T16:30:00Z", - "deliveryEnd": "2025-07-06T16:45:00Z", + "deliveryStart": "2025-10-01T16:30:00Z", + "deliveryEnd": "2025-10-01T16:45:00Z", "entryPerArea": { - "SE3": 431.84 + "SE3": 2611.11 } }, { - "deliveryStart": "2025-07-06T16:45:00Z", - "deliveryEnd": "2025-07-06T17:00:00Z", + "deliveryStart": "2025-10-01T16:45:00Z", + "deliveryEnd": "2025-10-01T17:00:00Z", "entryPerArea": { - "SE3": 431.84 + "SE3": 3467.41 } }, { - "deliveryStart": "2025-07-06T17:00:00Z", - "deliveryEnd": "2025-07-06T17:15:00Z", + "deliveryStart": "2025-10-01T17:00:00Z", + "deliveryEnd": "2025-10-01T17:15:00Z", "entryPerArea": { - "SE3": 423.73 + "SE3": 3828.03 } }, { - "deliveryStart": "2025-07-06T17:15:00Z", - "deliveryEnd": "2025-07-06T17:30:00Z", + "deliveryStart": "2025-10-01T17:15:00Z", + "deliveryEnd": "2025-10-01T17:30:00Z", "entryPerArea": { - "SE3": 423.73 + "SE3": 3429.83 } }, { - "deliveryStart": "2025-07-06T17:30:00Z", - "deliveryEnd": "2025-07-06T17:45:00Z", + "deliveryStart": "2025-10-01T17:30:00Z", + "deliveryEnd": "2025-10-01T17:45:00Z", "entryPerArea": { - "SE3": 423.73 + "SE3": 2934.38 } }, { - "deliveryStart": "2025-07-06T17:45:00Z", - "deliveryEnd": "2025-07-06T18:00:00Z", + "deliveryStart": "2025-10-01T17:45:00Z", + "deliveryEnd": "2025-10-01T18:00:00Z", "entryPerArea": { - "SE3": 423.73 + "SE3": 2308.07 } }, { - "deliveryStart": "2025-07-06T18:00:00Z", - "deliveryEnd": "2025-07-06T18:15:00Z", + "deliveryStart": "2025-10-01T18:00:00Z", + "deliveryEnd": "2025-10-01T18:15:00Z", "entryPerArea": { - "SE3": 437.92 + "SE3": 1997.96 } }, { - "deliveryStart": "2025-07-06T18:15:00Z", - "deliveryEnd": "2025-07-06T18:30:00Z", + "deliveryStart": "2025-10-01T18:15:00Z", + "deliveryEnd": "2025-10-01T18:30:00Z", "entryPerArea": { - "SE3": 437.92 + "SE3": 1424.03 } }, { - "deliveryStart": "2025-07-06T18:30:00Z", - "deliveryEnd": "2025-07-06T18:45:00Z", + "deliveryStart": "2025-10-01T18:30:00Z", + "deliveryEnd": "2025-10-01T18:45:00Z", "entryPerArea": { - "SE3": 437.92 + "SE3": 1216.81 } }, { - "deliveryStart": "2025-07-06T18:45:00Z", - "deliveryEnd": "2025-07-06T19:00:00Z", + "deliveryStart": "2025-10-01T18:45:00Z", + "deliveryEnd": "2025-10-01T19:00:00Z", "entryPerArea": { - "SE3": 437.92 + "SE3": 1070.15 } }, { - "deliveryStart": "2025-07-06T19:00:00Z", - "deliveryEnd": "2025-07-06T19:15:00Z", + "deliveryStart": "2025-10-01T19:00:00Z", + "deliveryEnd": "2025-10-01T19:15:00Z", "entryPerArea": { - "SE3": 416.42 + "SE3": 1218.14 } }, { - "deliveryStart": "2025-07-06T19:15:00Z", - "deliveryEnd": "2025-07-06T19:30:00Z", + "deliveryStart": "2025-10-01T19:15:00Z", + "deliveryEnd": "2025-10-01T19:30:00Z", "entryPerArea": { - "SE3": 416.42 + "SE3": 1135.8 } }, { - "deliveryStart": "2025-07-06T19:30:00Z", - "deliveryEnd": "2025-07-06T19:45:00Z", + "deliveryStart": "2025-10-01T19:30:00Z", + "deliveryEnd": "2025-10-01T19:45:00Z", "entryPerArea": { - "SE3": 416.42 + "SE3": 959.96 } }, { - "deliveryStart": "2025-07-06T19:45:00Z", - "deliveryEnd": "2025-07-06T20:00:00Z", + "deliveryStart": "2025-10-01T19:45:00Z", + "deliveryEnd": "2025-10-01T20:00:00Z", "entryPerArea": { - "SE3": 416.42 + "SE3": 913.66 } }, { - "deliveryStart": "2025-07-06T20:00:00Z", - "deliveryEnd": "2025-07-06T20:15:00Z", + "deliveryStart": "2025-10-01T20:00:00Z", + "deliveryEnd": "2025-10-01T20:15:00Z", "entryPerArea": { - "SE3": 414.39 + "SE3": 1001.63 } }, { - "deliveryStart": "2025-07-06T20:15:00Z", - "deliveryEnd": "2025-07-06T20:30:00Z", + "deliveryStart": "2025-10-01T20:15:00Z", + "deliveryEnd": "2025-10-01T20:30:00Z", "entryPerArea": { - "SE3": 414.39 + "SE3": 933.0 } }, { - "deliveryStart": "2025-07-06T20:30:00Z", - "deliveryEnd": "2025-07-06T20:45:00Z", + "deliveryStart": "2025-10-01T20:30:00Z", + "deliveryEnd": "2025-10-01T20:45:00Z", "entryPerArea": { - "SE3": 414.39 + "SE3": 874.53 } }, { - "deliveryStart": "2025-07-06T20:45:00Z", - "deliveryEnd": "2025-07-06T21:00:00Z", + "deliveryStart": "2025-10-01T20:45:00Z", + "deliveryEnd": "2025-10-01T21:00:00Z", "entryPerArea": { - "SE3": 414.39 + "SE3": 821.71 } }, { - "deliveryStart": "2025-07-06T21:00:00Z", - "deliveryEnd": "2025-07-06T21:15:00Z", + "deliveryStart": "2025-10-01T21:00:00Z", + "deliveryEnd": "2025-10-01T21:15:00Z", "entryPerArea": { - "SE3": 396.38 + "SE3": 860.5 } }, { - "deliveryStart": "2025-07-06T21:15:00Z", - "deliveryEnd": "2025-07-06T21:30:00Z", + "deliveryStart": "2025-10-01T21:15:00Z", + "deliveryEnd": "2025-10-01T21:30:00Z", "entryPerArea": { - "SE3": 396.38 + "SE3": 840.16 } }, { - "deliveryStart": "2025-07-06T21:30:00Z", - "deliveryEnd": "2025-07-06T21:45:00Z", + "deliveryStart": "2025-10-01T21:30:00Z", + "deliveryEnd": "2025-10-01T21:45:00Z", "entryPerArea": { - "SE3": 396.38 + "SE3": 820.05 } }, { - "deliveryStart": "2025-07-06T21:45:00Z", - "deliveryEnd": "2025-07-06T22:00:00Z", + "deliveryStart": "2025-10-01T21:45:00Z", + "deliveryEnd": "2025-10-01T22:00:00Z", "entryPerArea": { - "SE3": 396.38 + "SE3": 785.68 } } ] diff --git a/tests/components/nordpool/fixtures/indices_60.json b/tests/components/nordpool/fixtures/indices_60.json index 97bbe554b13da6..d9df6671d897e7 100644 --- a/tests/components/nordpool/fixtures/indices_60.json +++ b/tests/components/nordpool/fixtures/indices_60.json @@ -1,184 +1,184 @@ { - "deliveryDateCET": "2025-07-06", - "version": 2, - "updatedAt": "2025-07-05T10:56:44.6936838Z", + "deliveryDateCET": "2025-10-01", + "version": 3, + "updatedAt": "2025-09-30T12:08:22.6180024Z", "market": "DayAhead", "indexNames": ["SE3"], "currency": "SEK", "resolutionInMinutes": 60, "areaStates": [ { - "state": "Preliminary", + "state": "Final", "areas": ["SE3"] } ], "multiIndexEntries": [ { - "deliveryStart": "2025-07-05T22:00:00Z", - "deliveryEnd": "2025-07-05T23:00:00Z", + "deliveryStart": "2025-09-30T22:00:00Z", + "deliveryEnd": "2025-09-30T23:00:00Z", "entryPerArea": { - "SE3": 43.57 + "SE3": 523.75 } }, { - "deliveryStart": "2025-07-05T23:00:00Z", - "deliveryEnd": "2025-07-06T00:00:00Z", + "deliveryStart": "2025-09-30T23:00:00Z", + "deliveryEnd": "2025-10-01T00:00:00Z", "entryPerArea": { - "SE3": 36.47 + "SE3": 485.95 } }, { - "deliveryStart": "2025-07-06T00:00:00Z", - "deliveryEnd": "2025-07-06T01:00:00Z", + "deliveryStart": "2025-10-01T00:00:00Z", + "deliveryEnd": "2025-10-01T01:00:00Z", "entryPerArea": { - "SE3": 35.57 + "SE3": 504.85 } }, { - "deliveryStart": "2025-07-06T01:00:00Z", - "deliveryEnd": "2025-07-06T02:00:00Z", + "deliveryStart": "2025-10-01T01:00:00Z", + "deliveryEnd": "2025-10-01T02:00:00Z", "entryPerArea": { - "SE3": 30.73 + "SE3": 442.07 } }, { - "deliveryStart": "2025-07-06T02:00:00Z", - "deliveryEnd": "2025-07-06T03:00:00Z", + "deliveryStart": "2025-10-01T02:00:00Z", + "deliveryEnd": "2025-10-01T03:00:00Z", "entryPerArea": { - "SE3": 32.42 + "SE3": 496.12 } }, { - "deliveryStart": "2025-07-06T03:00:00Z", - "deliveryEnd": "2025-07-06T04:00:00Z", + "deliveryStart": "2025-10-01T03:00:00Z", + "deliveryEnd": "2025-10-01T04:00:00Z", "entryPerArea": { - "SE3": 38.73 + "SE3": 670.85 } }, { - "deliveryStart": "2025-07-06T04:00:00Z", - "deliveryEnd": "2025-07-06T05:00:00Z", + "deliveryStart": "2025-10-01T04:00:00Z", + "deliveryEnd": "2025-10-01T05:00:00Z", "entryPerArea": { - "SE3": 42.78 + "SE3": 1149.72 } }, { - "deliveryStart": "2025-07-06T05:00:00Z", - "deliveryEnd": "2025-07-06T06:00:00Z", + "deliveryStart": "2025-10-01T05:00:00Z", + "deliveryEnd": "2025-10-01T06:00:00Z", "entryPerArea": { - "SE3": 54.71 + "SE3": 1694.25 } }, { - "deliveryStart": "2025-07-06T06:00:00Z", - "deliveryEnd": "2025-07-06T07:00:00Z", + "deliveryStart": "2025-10-01T06:00:00Z", + "deliveryEnd": "2025-10-01T07:00:00Z", "entryPerArea": { - "SE3": 83.87 + "SE3": 1378.06 } }, { - "deliveryStart": "2025-07-06T07:00:00Z", - "deliveryEnd": "2025-07-06T08:00:00Z", + "deliveryStart": "2025-10-01T07:00:00Z", + "deliveryEnd": "2025-10-01T08:00:00Z", "entryPerArea": { - "SE3": 78.8 + "SE3": 1063.41 } }, { - "deliveryStart": "2025-07-06T08:00:00Z", - "deliveryEnd": "2025-07-06T09:00:00Z", + "deliveryStart": "2025-10-01T08:00:00Z", + "deliveryEnd": "2025-10-01T09:00:00Z", "entryPerArea": { - "SE3": 92.09 + "SE3": 874.53 } }, { - "deliveryStart": "2025-07-06T09:00:00Z", - "deliveryEnd": "2025-07-06T10:00:00Z", + "deliveryStart": "2025-10-01T09:00:00Z", + "deliveryEnd": "2025-10-01T10:00:00Z", "entryPerArea": { - "SE3": 104.92 + "SE3": 708.2 } }, { - "deliveryStart": "2025-07-06T10:00:00Z", - "deliveryEnd": "2025-07-06T11:00:00Z", + "deliveryStart": "2025-10-01T10:00:00Z", + "deliveryEnd": "2025-10-01T11:00:00Z", "entryPerArea": { - "SE3": 72.5 + "SE3": 646.53 } }, { - "deliveryStart": "2025-07-06T11:00:00Z", - "deliveryEnd": "2025-07-06T12:00:00Z", + "deliveryStart": "2025-10-01T11:00:00Z", + "deliveryEnd": "2025-10-01T12:00:00Z", "entryPerArea": { - "SE3": 63.49 + "SE3": 667.97 } }, { - "deliveryStart": "2025-07-06T12:00:00Z", - "deliveryEnd": "2025-07-06T13:00:00Z", + "deliveryStart": "2025-10-01T12:00:00Z", + "deliveryEnd": "2025-10-01T13:00:00Z", "entryPerArea": { - "SE3": 91.64 + "SE3": 710.63 } }, { - "deliveryStart": "2025-07-06T13:00:00Z", - "deliveryEnd": "2025-07-06T14:00:00Z", + "deliveryStart": "2025-10-01T13:00:00Z", + "deliveryEnd": "2025-10-01T14:00:00Z", "entryPerArea": { - "SE3": 111.79 + "SE3": 807.23 } }, { - "deliveryStart": "2025-07-06T14:00:00Z", - "deliveryEnd": "2025-07-06T15:00:00Z", + "deliveryStart": "2025-10-01T14:00:00Z", + "deliveryEnd": "2025-10-01T15:00:00Z", "entryPerArea": { - "SE3": 234.04 + "SE3": 909.35 } }, { - "deliveryStart": "2025-07-06T15:00:00Z", - "deliveryEnd": "2025-07-06T16:00:00Z", + "deliveryStart": "2025-10-01T15:00:00Z", + "deliveryEnd": "2025-10-01T16:00:00Z", "entryPerArea": { - "SE3": 435.33 + "SE3": 1388.22 } }, { - "deliveryStart": "2025-07-06T16:00:00Z", - "deliveryEnd": "2025-07-06T17:00:00Z", + "deliveryStart": "2025-10-01T16:00:00Z", + "deliveryEnd": "2025-10-01T17:00:00Z", "entryPerArea": { - "SE3": 431.84 + "SE3": 2350.51 } }, { - "deliveryStart": "2025-07-06T17:00:00Z", - "deliveryEnd": "2025-07-06T18:00:00Z", + "deliveryStart": "2025-10-01T17:00:00Z", + "deliveryEnd": "2025-10-01T18:00:00Z", "entryPerArea": { - "SE3": 423.73 + "SE3": 3125.13 } }, { - "deliveryStart": "2025-07-06T18:00:00Z", - "deliveryEnd": "2025-07-06T19:00:00Z", + "deliveryStart": "2025-10-01T18:00:00Z", + "deliveryEnd": "2025-10-01T19:00:00Z", "entryPerArea": { - "SE3": 437.92 + "SE3": 1427.24 } }, { - "deliveryStart": "2025-07-06T19:00:00Z", - "deliveryEnd": "2025-07-06T20:00:00Z", + "deliveryStart": "2025-10-01T19:00:00Z", + "deliveryEnd": "2025-10-01T20:00:00Z", "entryPerArea": { - "SE3": 416.42 + "SE3": 1056.89 } }, { - "deliveryStart": "2025-07-06T20:00:00Z", - "deliveryEnd": "2025-07-06T21:00:00Z", + "deliveryStart": "2025-10-01T20:00:00Z", + "deliveryEnd": "2025-10-01T21:00:00Z", "entryPerArea": { - "SE3": 414.39 + "SE3": 907.69 } }, { - "deliveryStart": "2025-07-06T21:00:00Z", - "deliveryEnd": "2025-07-06T22:00:00Z", + "deliveryStart": "2025-10-01T21:00:00Z", + "deliveryEnd": "2025-10-01T22:00:00Z", "entryPerArea": { - "SE3": 396.38 + "SE3": 826.57 } } ] diff --git a/tests/components/nordpool/snapshots/test_diagnostics.ambr b/tests/components/nordpool/snapshots/test_diagnostics.ambr index d7f7c4041cd7f3..a4434a1246a21e 100644 --- a/tests/components/nordpool/snapshots/test_diagnostics.ambr +++ b/tests/components/nordpool/snapshots/test_diagnostics.ambr @@ -2,15 +2,15 @@ # name: test_diagnostics dict({ 'raw': dict({ - '2024-11-04': dict({ + '2025-09-30': dict({ 'areaAverages': list([ dict({ 'areaCode': 'SE3', - 'price': 1006.88, + 'price': 974.99, }), dict({ 'areaCode': 'SE4', - 'price': 1190.46, + 'price': 1135.54, }), ]), 'areaStates': list([ @@ -26,53 +26,53 @@ dict({ 'averagePricePerArea': dict({ 'SE3': dict({ - 'average': 408.69, - 'max': 1426.17, - 'min': 66.13, + 'average': 594.26, + 'max': 1909.0, + 'min': 242.43, }), 'SE4': dict({ - 'average': 530.61, - 'max': 1695.95, - 'min': 78.59, + 'average': 713.53, + 'max': 2247.98, + 'min': 311.51, }), }), 'blockName': 'Off-peak 1', - 'deliveryEnd': '2024-11-04T07:00:00Z', - 'deliveryStart': '2024-11-03T23:00:00Z', + 'deliveryEnd': '2025-09-30T06:00:00Z', + 'deliveryStart': '2025-09-29T22:00:00Z', }), dict({ 'averagePricePerArea': dict({ 'SE3': dict({ - 'average': 1455.48, - 'max': 2812.53, - 'min': 952.76, + 'average': 1249.39, + 'max': 2652.18, + 'min': 821.53, }), 'SE4': dict({ - 'average': 1692.2, - 'max': 3313.53, - 'min': 1085.73, + 'average': 1439.73, + 'max': 3083.2, + 'min': 927.24, }), }), 'blockName': 'Peak', - 'deliveryEnd': '2024-11-04T19:00:00Z', - 'deliveryStart': '2024-11-04T07:00:00Z', + 'deliveryEnd': '2025-09-30T18:00:00Z', + 'deliveryStart': '2025-09-30T06:00:00Z', }), dict({ 'averagePricePerArea': dict({ 'SE3': dict({ - 'average': 857.49, - 'max': 1165.02, - 'min': 465.38, + 'average': 913.23, + 'max': 1109.76, + 'min': 697.17, }), 'SE4': dict({ - 'average': 1004.95, - 'max': 1398.35, - 'min': 528.83, + 'average': 1067.0, + 'max': 1305.73, + 'min': 812.37, }), }), 'blockName': 'Off-peak 2', - 'deliveryEnd': '2024-11-04T23:00:00Z', - 'deliveryStart': '2024-11-04T19:00:00Z', + 'deliveryEnd': '2025-09-30T22:00:00Z', + 'deliveryStart': '2025-09-30T18:00:00Z', }), ]), 'currency': 'SEK', @@ -80,215 +80,215 @@ 'SE3', 'SE4', ]), - 'deliveryDateCET': '2024-11-04', - 'exchangeRate': 11.64318, + 'deliveryDateCET': '2025-09-30', + 'exchangeRate': 11.03467, 'market': 'DayAhead', 'multiAreaEntries': list([ dict({ - 'deliveryEnd': '2024-11-04T00:00:00Z', - 'deliveryStart': '2024-11-03T23:00:00Z', + 'deliveryEnd': '2025-09-29T23:00:00Z', + 'deliveryStart': '2025-09-29T22:00:00Z', 'entryPerArea': dict({ - 'SE3': 66.13, - 'SE4': 78.59, + 'SE3': 278.63, + 'SE4': 354.65, }), }), dict({ - 'deliveryEnd': '2024-11-04T01:00:00Z', - 'deliveryStart': '2024-11-04T00:00:00Z', + 'deliveryEnd': '2025-09-30T00:00:00Z', + 'deliveryStart': '2025-09-29T23:00:00Z', 'entryPerArea': dict({ - 'SE3': 72.54, - 'SE4': 86.51, + 'SE3': 261.85, + 'SE4': 336.89, }), }), dict({ - 'deliveryEnd': '2024-11-04T02:00:00Z', - 'deliveryStart': '2024-11-04T01:00:00Z', + 'deliveryEnd': '2025-09-30T01:00:00Z', + 'deliveryStart': '2025-09-30T00:00:00Z', 'entryPerArea': dict({ - 'SE3': 73.12, - 'SE4': 84.88, + 'SE3': 242.43, + 'SE4': 313.16, }), }), dict({ - 'deliveryEnd': '2024-11-04T03:00:00Z', - 'deliveryStart': '2024-11-04T02:00:00Z', + 'deliveryEnd': '2025-09-30T02:00:00Z', + 'deliveryStart': '2025-09-30T01:00:00Z', 'entryPerArea': dict({ - 'SE3': 171.97, - 'SE4': 217.26, + 'SE3': 322.65, + 'SE4': 401.0, }), }), dict({ - 'deliveryEnd': '2024-11-04T04:00:00Z', - 'deliveryStart': '2024-11-04T03:00:00Z', + 'deliveryEnd': '2025-09-30T03:00:00Z', + 'deliveryStart': '2025-09-30T02:00:00Z', 'entryPerArea': dict({ - 'SE3': 181.05, - 'SE4': 227.74, + 'SE3': 243.2, + 'SE4': 311.51, }), }), dict({ - 'deliveryEnd': '2024-11-04T05:00:00Z', - 'deliveryStart': '2024-11-04T04:00:00Z', + 'deliveryEnd': '2025-09-30T04:00:00Z', + 'deliveryStart': '2025-09-30T03:00:00Z', 'entryPerArea': dict({ - 'SE3': 360.71, - 'SE4': 414.61, + 'SE3': 596.53, + 'SE4': 695.52, }), }), dict({ - 'deliveryEnd': '2024-11-04T06:00:00Z', - 'deliveryStart': '2024-11-04T05:00:00Z', + 'deliveryEnd': '2025-09-30T05:00:00Z', + 'deliveryStart': '2025-09-30T04:00:00Z', 'entryPerArea': dict({ - 'SE3': 917.83, - 'SE4': 1439.33, + 'SE3': 899.77, + 'SE4': 1047.52, }), }), dict({ - 'deliveryEnd': '2024-11-04T07:00:00Z', - 'deliveryStart': '2024-11-04T06:00:00Z', + 'deliveryEnd': '2025-09-30T06:00:00Z', + 'deliveryStart': '2025-09-30T05:00:00Z', 'entryPerArea': dict({ - 'SE3': 1426.17, - 'SE4': 1695.95, + 'SE3': 1909.0, + 'SE4': 2247.98, }), }), dict({ - 'deliveryEnd': '2024-11-04T08:00:00Z', - 'deliveryStart': '2024-11-04T07:00:00Z', + 'deliveryEnd': '2025-09-30T07:00:00Z', + 'deliveryStart': '2025-09-30T06:00:00Z', 'entryPerArea': dict({ - 'SE3': 1350.96, - 'SE4': 1605.13, + 'SE3': 1432.52, + 'SE4': 1681.24, }), }), dict({ - 'deliveryEnd': '2024-11-04T09:00:00Z', - 'deliveryStart': '2024-11-04T08:00:00Z', + 'deliveryEnd': '2025-09-30T08:00:00Z', + 'deliveryStart': '2025-09-30T07:00:00Z', 'entryPerArea': dict({ - 'SE3': 1195.06, - 'SE4': 1393.46, + 'SE3': 1127.52, + 'SE4': 1304.96, }), }), dict({ - 'deliveryEnd': '2024-11-04T10:00:00Z', - 'deliveryStart': '2024-11-04T09:00:00Z', + 'deliveryEnd': '2025-09-30T09:00:00Z', + 'deliveryStart': '2025-09-30T08:00:00Z', 'entryPerArea': dict({ - 'SE3': 992.35, - 'SE4': 1126.71, + 'SE3': 966.75, + 'SE4': 1073.34, }), }), dict({ - 'deliveryEnd': '2024-11-04T11:00:00Z', - 'deliveryStart': '2024-11-04T10:00:00Z', + 'deliveryEnd': '2025-09-30T10:00:00Z', + 'deliveryStart': '2025-09-30T09:00:00Z', 'entryPerArea': dict({ - 'SE3': 976.63, - 'SE4': 1107.97, + 'SE3': 882.55, + 'SE4': 1003.93, }), }), dict({ - 'deliveryEnd': '2024-11-04T12:00:00Z', - 'deliveryStart': '2024-11-04T11:00:00Z', + 'deliveryEnd': '2025-09-30T11:00:00Z', + 'deliveryStart': '2025-09-30T10:00:00Z', 'entryPerArea': dict({ - 'SE3': 952.76, - 'SE4': 1085.73, + 'SE3': 841.72, + 'SE4': 947.44, }), }), dict({ - 'deliveryEnd': '2024-11-04T13:00:00Z', - 'deliveryStart': '2024-11-04T12:00:00Z', + 'deliveryEnd': '2025-09-30T12:00:00Z', + 'deliveryStart': '2025-09-30T11:00:00Z', 'entryPerArea': dict({ - 'SE3': 1029.37, - 'SE4': 1177.71, + 'SE3': 821.53, + 'SE4': 927.24, }), }), dict({ - 'deliveryEnd': '2024-11-04T14:00:00Z', - 'deliveryStart': '2024-11-04T13:00:00Z', + 'deliveryEnd': '2025-09-30T13:00:00Z', + 'deliveryStart': '2025-09-30T12:00:00Z', 'entryPerArea': dict({ - 'SE3': 1043.35, - 'SE4': 1194.59, + 'SE3': 864.35, + 'SE4': 970.5, }), }), dict({ - 'deliveryEnd': '2024-11-04T15:00:00Z', - 'deliveryStart': '2024-11-04T14:00:00Z', + 'deliveryEnd': '2025-09-30T14:00:00Z', + 'deliveryStart': '2025-09-30T13:00:00Z', 'entryPerArea': dict({ - 'SE3': 1359.57, - 'SE4': 1561.12, + 'SE3': 931.88, + 'SE4': 1046.64, }), }), dict({ - 'deliveryEnd': '2024-11-04T16:00:00Z', - 'deliveryStart': '2024-11-04T15:00:00Z', + 'deliveryEnd': '2025-09-30T15:00:00Z', + 'deliveryStart': '2025-09-30T14:00:00Z', 'entryPerArea': dict({ - 'SE3': 1848.35, - 'SE4': 2145.84, + 'SE3': 1039.13, + 'SE4': 1165.04, }), }), dict({ - 'deliveryEnd': '2024-11-04T17:00:00Z', - 'deliveryStart': '2024-11-04T16:00:00Z', + 'deliveryEnd': '2025-09-30T16:00:00Z', + 'deliveryStart': '2025-09-30T15:00:00Z', 'entryPerArea': dict({ - 'SE3': 2812.53, - 'SE4': 3313.53, + 'SE3': 1296.57, + 'SE4': 1520.91, }), }), dict({ - 'deliveryEnd': '2024-11-04T18:00:00Z', - 'deliveryStart': '2024-11-04T17:00:00Z', + 'deliveryEnd': '2025-09-30T17:00:00Z', + 'deliveryStart': '2025-09-30T16:00:00Z', 'entryPerArea': dict({ - 'SE3': 2351.69, - 'SE4': 2751.87, + 'SE3': 2652.18, + 'SE4': 3083.2, }), }), dict({ - 'deliveryEnd': '2024-11-04T19:00:00Z', - 'deliveryStart': '2024-11-04T18:00:00Z', + 'deliveryEnd': '2025-09-30T18:00:00Z', + 'deliveryStart': '2025-09-30T17:00:00Z', 'entryPerArea': dict({ - 'SE3': 1553.08, - 'SE4': 1842.77, + 'SE3': 2135.98, + 'SE4': 2552.32, }), }), dict({ - 'deliveryEnd': '2024-11-04T20:00:00Z', - 'deliveryStart': '2024-11-04T19:00:00Z', + 'deliveryEnd': '2025-09-30T19:00:00Z', + 'deliveryStart': '2025-09-30T18:00:00Z', 'entryPerArea': dict({ - 'SE3': 1165.02, - 'SE4': 1398.35, + 'SE3': 1109.76, + 'SE4': 1305.73, }), }), dict({ - 'deliveryEnd': '2024-11-04T21:00:00Z', - 'deliveryStart': '2024-11-04T20:00:00Z', + 'deliveryEnd': '2025-09-30T20:00:00Z', + 'deliveryStart': '2025-09-30T19:00:00Z', 'entryPerArea': dict({ - 'SE3': 1007.48, - 'SE4': 1172.35, + 'SE3': 973.81, + 'SE4': 1130.83, }), }), dict({ - 'deliveryEnd': '2024-11-04T22:00:00Z', - 'deliveryStart': '2024-11-04T21:00:00Z', + 'deliveryEnd': '2025-09-30T21:00:00Z', + 'deliveryStart': '2025-09-30T20:00:00Z', 'entryPerArea': dict({ - 'SE3': 792.09, - 'SE4': 920.28, + 'SE3': 872.18, + 'SE4': 1019.05, }), }), dict({ - 'deliveryEnd': '2024-11-04T23:00:00Z', - 'deliveryStart': '2024-11-04T22:00:00Z', + 'deliveryEnd': '2025-09-30T22:00:00Z', + 'deliveryStart': '2025-09-30T21:00:00Z', 'entryPerArea': dict({ - 'SE3': 465.38, - 'SE4': 528.83, + 'SE3': 697.17, + 'SE4': 812.37, }), }), ]), - 'updatedAt': '2024-11-04T08:09:11.1931991Z', + 'updatedAt': '2025-09-29T11:17:12.3019385Z', 'version': 3, }), - '2024-11-05': dict({ + '2025-10-01': dict({ 'areaAverages': list([ dict({ 'areaCode': 'SE3', - 'price': 900.74, + 'price': 1033.98, }), dict({ 'areaCode': 'SE4', - 'price': 1166.12, + 'price': 1180.78, }), ]), 'areaStates': list([ @@ -304,53 +304,53 @@ dict({ 'averagePricePerArea': dict({ 'SE3': dict({ - 'average': 422.87, - 'max': 1406.14, - 'min': 61.69, + 'average': 745.93, + 'max': 1809.96, + 'min': 441.96, }), 'SE4': dict({ - 'average': 497.97, - 'max': 1648.25, - 'min': 65.19, + 'average': 860.99, + 'max': 2029.34, + 'min': 515.46, }), }), 'blockName': 'Off-peak 1', - 'deliveryEnd': '2024-11-05T07:00:00Z', - 'deliveryStart': '2024-11-04T23:00:00Z', + 'deliveryEnd': '2025-10-01T06:00:00Z', + 'deliveryStart': '2025-09-30T22:00:00Z', }), dict({ 'averagePricePerArea': dict({ 'SE3': dict({ - 'average': 1315.97, - 'max': 2512.65, - 'min': 925.05, + 'average': 1219.13, + 'max': 3828.03, + 'min': 607.74, }), 'SE4': dict({ - 'average': 1735.59, - 'max': 3533.03, - 'min': 1081.72, + 'average': 1381.22, + 'max': 4442.74, + 'min': 683.12, }), }), 'blockName': 'Peak', - 'deliveryEnd': '2024-11-05T19:00:00Z', - 'deliveryStart': '2024-11-05T07:00:00Z', + 'deliveryEnd': '2025-10-01T18:00:00Z', + 'deliveryStart': '2025-10-01T06:00:00Z', }), dict({ 'averagePricePerArea': dict({ 'SE3': dict({ - 'average': 610.79, - 'max': 835.53, - 'min': 289.14, + 'average': 1054.61, + 'max': 1997.96, + 'min': 785.68, }), 'SE4': dict({ - 'average': 793.98, - 'max': 1112.57, - 'min': 349.21, + 'average': 1219.07, + 'max': 2312.16, + 'min': 912.22, }), }), 'blockName': 'Off-peak 2', - 'deliveryEnd': '2024-11-05T23:00:00Z', - 'deliveryStart': '2024-11-05T19:00:00Z', + 'deliveryEnd': '2025-10-01T22:00:00Z', + 'deliveryStart': '2025-10-01T18:00:00Z', }), ]), 'currency': 'SEK', @@ -358,482 +358,1634 @@ 'SE3', 'SE4', ]), - 'deliveryDateCET': '2024-11-05', - 'exchangeRate': 11.6402, + 'deliveryDateCET': '2025-10-01', + 'exchangeRate': 11.05186, 'market': 'DayAhead', 'multiAreaEntries': list([ dict({ - 'deliveryEnd': '2024-11-05T00:00:00Z', - 'deliveryStart': '2024-11-04T23:00:00Z', + 'deliveryEnd': '2025-09-30T22:15:00Z', + 'deliveryStart': '2025-09-30T22:00:00Z', 'entryPerArea': dict({ - 'SE3': 250.73, - 'SE4': 283.79, + 'SE3': 556.68, + 'SE4': 642.22, }), }), dict({ - 'deliveryEnd': '2024-11-05T01:00:00Z', - 'deliveryStart': '2024-11-05T00:00:00Z', + 'deliveryEnd': '2025-09-30T22:30:00Z', + 'deliveryStart': '2025-09-30T22:15:00Z', 'entryPerArea': dict({ - 'SE3': 76.36, - 'SE4': 81.36, + 'SE3': 519.88, + 'SE4': 600.12, }), }), dict({ - 'deliveryEnd': '2024-11-05T02:00:00Z', - 'deliveryStart': '2024-11-05T01:00:00Z', + 'deliveryEnd': '2025-09-30T22:45:00Z', + 'deliveryStart': '2025-09-30T22:30:00Z', 'entryPerArea': dict({ - 'SE3': 73.92, - 'SE4': 79.15, + 'SE3': 508.28, + 'SE4': 586.3, }), }), dict({ - 'deliveryEnd': '2024-11-05T03:00:00Z', - 'deliveryStart': '2024-11-05T02:00:00Z', + 'deliveryEnd': '2025-09-30T23:00:00Z', + 'deliveryStart': '2025-09-30T22:45:00Z', 'entryPerArea': dict({ - 'SE3': 61.69, - 'SE4': 65.19, + 'SE3': 509.93, + 'SE4': 589.62, }), }), dict({ - 'deliveryEnd': '2024-11-05T04:00:00Z', - 'deliveryStart': '2024-11-05T03:00:00Z', + 'deliveryEnd': '2025-09-30T23:15:00Z', + 'deliveryStart': '2025-09-30T23:00:00Z', 'entryPerArea': dict({ - 'SE3': 64.6, - 'SE4': 68.44, + 'SE3': 501.64, + 'SE4': 577.24, }), }), dict({ - 'deliveryEnd': '2024-11-05T05:00:00Z', - 'deliveryStart': '2024-11-05T04:00:00Z', + 'deliveryEnd': '2025-09-30T23:30:00Z', + 'deliveryStart': '2025-09-30T23:15:00Z', 'entryPerArea': dict({ - 'SE3': 453.27, - 'SE4': 516.71, + 'SE3': 509.05, + 'SE4': 585.42, }), }), dict({ - 'deliveryEnd': '2024-11-05T06:00:00Z', - 'deliveryStart': '2024-11-05T05:00:00Z', + 'deliveryEnd': '2025-09-30T23:45:00Z', + 'deliveryStart': '2025-09-30T23:30:00Z', 'entryPerArea': dict({ - 'SE3': 996.28, - 'SE4': 1240.85, + 'SE3': 491.03, + 'SE4': 567.18, }), }), dict({ - 'deliveryEnd': '2024-11-05T07:00:00Z', - 'deliveryStart': '2024-11-05T06:00:00Z', + 'deliveryEnd': '2025-10-01T00:00:00Z', + 'deliveryStart': '2025-09-30T23:45:00Z', 'entryPerArea': dict({ - 'SE3': 1406.14, - 'SE4': 1648.25, + 'SE3': 442.07, + 'SE4': 517.45, }), }), dict({ - 'deliveryEnd': '2024-11-05T08:00:00Z', - 'deliveryStart': '2024-11-05T07:00:00Z', + 'deliveryEnd': '2025-10-01T00:15:00Z', + 'deliveryStart': '2025-10-01T00:00:00Z', 'entryPerArea': dict({ - 'SE3': 1346.54, - 'SE4': 1570.5, + 'SE3': 504.08, + 'SE4': 580.55, }), }), dict({ - 'deliveryEnd': '2024-11-05T09:00:00Z', - 'deliveryStart': '2024-11-05T08:00:00Z', + 'deliveryEnd': '2025-10-01T00:30:00Z', + 'deliveryStart': '2025-10-01T00:15:00Z', 'entryPerArea': dict({ - 'SE3': 1150.28, - 'SE4': 1345.37, + 'SE3': 504.85, + 'SE4': 581.55, }), }), dict({ - 'deliveryEnd': '2024-11-05T10:00:00Z', - 'deliveryStart': '2024-11-05T09:00:00Z', + 'deliveryEnd': '2025-10-01T00:45:00Z', + 'deliveryStart': '2025-10-01T00:30:00Z', 'entryPerArea': dict({ - 'SE3': 1031.32, - 'SE4': 1206.51, + 'SE3': 504.3, + 'SE4': 580.78, }), }), dict({ - 'deliveryEnd': '2024-11-05T11:00:00Z', - 'deliveryStart': '2024-11-05T10:00:00Z', + 'deliveryEnd': '2025-10-01T01:00:00Z', + 'deliveryStart': '2025-10-01T00:45:00Z', 'entryPerArea': dict({ - 'SE3': 927.37, - 'SE4': 1085.8, + 'SE3': 506.29, + 'SE4': 583.1, }), }), dict({ - 'deliveryEnd': '2024-11-05T12:00:00Z', - 'deliveryStart': '2024-11-05T11:00:00Z', + 'deliveryEnd': '2025-10-01T01:15:00Z', + 'deliveryStart': '2025-10-01T01:00:00Z', 'entryPerArea': dict({ - 'SE3': 925.05, - 'SE4': 1081.72, + 'SE3': 442.07, + 'SE4': 515.46, }), }), dict({ - 'deliveryEnd': '2024-11-05T13:00:00Z', - 'deliveryStart': '2024-11-05T12:00:00Z', + 'deliveryEnd': '2025-10-01T01:30:00Z', + 'deliveryStart': '2025-10-01T01:15:00Z', 'entryPerArea': dict({ - 'SE3': 949.49, - 'SE4': 1130.38, + 'SE3': 441.96, + 'SE4': 517.23, }), }), dict({ - 'deliveryEnd': '2024-11-05T14:00:00Z', - 'deliveryStart': '2024-11-05T13:00:00Z', + 'deliveryEnd': '2025-10-01T01:45:00Z', + 'deliveryStart': '2025-10-01T01:30:00Z', 'entryPerArea': dict({ - 'SE3': 1042.03, - 'SE4': 1256.91, + 'SE3': 442.07, + 'SE4': 516.23, }), }), dict({ - 'deliveryEnd': '2024-11-05T15:00:00Z', - 'deliveryStart': '2024-11-05T14:00:00Z', + 'deliveryEnd': '2025-10-01T02:00:00Z', + 'deliveryStart': '2025-10-01T01:45:00Z', 'entryPerArea': dict({ - 'SE3': 1258.89, - 'SE4': 1765.82, + 'SE3': 442.07, + 'SE4': 516.23, }), }), dict({ - 'deliveryEnd': '2024-11-05T16:00:00Z', - 'deliveryStart': '2024-11-05T15:00:00Z', + 'deliveryEnd': '2025-10-01T02:15:00Z', + 'deliveryStart': '2025-10-01T02:00:00Z', 'entryPerArea': dict({ - 'SE3': 1816.45, - 'SE4': 2522.55, + 'SE3': 441.96, + 'SE4': 517.34, }), }), dict({ - 'deliveryEnd': '2024-11-05T17:00:00Z', - 'deliveryStart': '2024-11-05T16:00:00Z', + 'deliveryEnd': '2025-10-01T02:30:00Z', + 'deliveryStart': '2025-10-01T02:15:00Z', 'entryPerArea': dict({ - 'SE3': 2512.65, - 'SE4': 3533.03, + 'SE3': 483.3, + 'SE4': 559.11, }), }), dict({ - 'deliveryEnd': '2024-11-05T18:00:00Z', - 'deliveryStart': '2024-11-05T17:00:00Z', + 'deliveryEnd': '2025-10-01T02:45:00Z', + 'deliveryStart': '2025-10-01T02:30:00Z', 'entryPerArea': dict({ - 'SE3': 1819.83, - 'SE4': 2524.06, + 'SE3': 484.29, + 'SE4': 559.0, }), }), dict({ - 'deliveryEnd': '2024-11-05T19:00:00Z', - 'deliveryStart': '2024-11-05T18:00:00Z', + 'deliveryEnd': '2025-10-01T03:00:00Z', + 'deliveryStart': '2025-10-01T02:45:00Z', 'entryPerArea': dict({ - 'SE3': 1011.77, + 'SE3': 574.7, + 'SE4': 659.35, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T03:15:00Z', + 'deliveryStart': '2025-10-01T03:00:00Z', + 'entryPerArea': dict({ + 'SE3': 543.31, + 'SE4': 631.95, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T03:30:00Z', + 'deliveryStart': '2025-10-01T03:15:00Z', + 'entryPerArea': dict({ + 'SE3': 578.01, + 'SE4': 671.18, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T03:45:00Z', + 'deliveryStart': '2025-10-01T03:30:00Z', + 'entryPerArea': dict({ + 'SE3': 774.96, + 'SE4': 893.1, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T04:00:00Z', + 'deliveryStart': '2025-10-01T03:45:00Z', + 'entryPerArea': dict({ + 'SE3': 787.0, + 'SE4': 909.79, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T04:15:00Z', + 'deliveryStart': '2025-10-01T04:00:00Z', + 'entryPerArea': dict({ + 'SE3': 902.38, + 'SE4': 1041.86, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T04:30:00Z', + 'deliveryStart': '2025-10-01T04:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1079.32, + 'SE4': 1254.17, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T04:45:00Z', + 'deliveryStart': '2025-10-01T04:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1222.67, + 'SE4': 1421.93, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T05:00:00Z', + 'deliveryStart': '2025-10-01T04:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1394.63, + 'SE4': 1623.08, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T05:15:00Z', + 'deliveryStart': '2025-10-01T05:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1529.36, + 'SE4': 1787.86, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T05:30:00Z', + 'deliveryStart': '2025-10-01T05:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1724.53, + 'SE4': 2015.75, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T05:45:00Z', + 'deliveryStart': '2025-10-01T05:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1809.96, + 'SE4': 2029.34, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T06:00:00Z', + 'deliveryStart': '2025-10-01T05:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1713.04, + 'SE4': 1920.15, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T06:15:00Z', + 'deliveryStart': '2025-10-01T06:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1925.9, + 'SE4': 2162.63, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T06:30:00Z', + 'deliveryStart': '2025-10-01T06:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1440.06, + 'SE4': 1614.01, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T06:45:00Z', + 'deliveryStart': '2025-10-01T06:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1183.32, + 'SE4': 1319.37, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T07:00:00Z', + 'deliveryStart': '2025-10-01T06:45:00Z', + 'entryPerArea': dict({ + 'SE3': 962.95, + 'SE4': 1068.71, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T07:15:00Z', + 'deliveryStart': '2025-10-01T07:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1402.04, + 'SE4': 1569.92, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T07:30:00Z', + 'deliveryStart': '2025-10-01T07:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1060.65, + 'SE4': 1178.46, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T07:45:00Z', + 'deliveryStart': '2025-10-01T07:30:00Z', + 'entryPerArea': dict({ + 'SE3': 949.13, + 'SE4': 1050.59, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T08:00:00Z', + 'deliveryStart': '2025-10-01T07:45:00Z', + 'entryPerArea': dict({ + 'SE3': 841.82, + 'SE4': 938.3, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T08:15:00Z', + 'deliveryStart': '2025-10-01T08:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1037.44, + 'SE4': 1141.44, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T08:30:00Z', + 'deliveryStart': '2025-10-01T08:15:00Z', + 'entryPerArea': dict({ + 'SE3': 950.13, + 'SE4': 1041.64, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T08:45:00Z', + 'deliveryStart': '2025-10-01T08:30:00Z', + 'entryPerArea': dict({ + 'SE3': 826.13, + 'SE4': 905.04, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T09:00:00Z', + 'deliveryStart': '2025-10-01T08:45:00Z', + 'entryPerArea': dict({ + 'SE3': 684.55, + 'SE4': 754.62, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T09:15:00Z', + 'deliveryStart': '2025-10-01T09:00:00Z', + 'entryPerArea': dict({ + 'SE3': 861.6, + 'SE4': 936.09, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T09:30:00Z', + 'deliveryStart': '2025-10-01T09:15:00Z', + 'entryPerArea': dict({ + 'SE3': 722.79, + 'SE4': 799.6, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T09:45:00Z', + 'deliveryStart': '2025-10-01T09:30:00Z', + 'entryPerArea': dict({ + 'SE3': 640.57, + 'SE4': 718.59, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T10:00:00Z', + 'deliveryStart': '2025-10-01T09:45:00Z', + 'entryPerArea': dict({ + 'SE3': 607.74, + 'SE4': 683.12, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T10:15:00Z', + 'deliveryStart': '2025-10-01T10:00:00Z', + 'entryPerArea': dict({ + 'SE3': 674.05, + 'SE4': 752.41, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T10:30:00Z', + 'deliveryStart': '2025-10-01T10:15:00Z', + 'entryPerArea': dict({ + 'SE3': 638.58, + 'SE4': 717.49, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T10:45:00Z', + 'deliveryStart': '2025-10-01T10:30:00Z', + 'entryPerArea': dict({ + 'SE3': 638.47, + 'SE4': 719.81, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T11:00:00Z', + 'deliveryStart': '2025-10-01T10:45:00Z', + 'entryPerArea': dict({ + 'SE3': 634.82, + 'SE4': 717.16, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T11:15:00Z', + 'deliveryStart': '2025-10-01T11:00:00Z', + 'entryPerArea': dict({ + 'SE3': 637.36, + 'SE4': 721.58, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T11:30:00Z', + 'deliveryStart': '2025-10-01T11:15:00Z', + 'entryPerArea': dict({ + 'SE3': 660.68, + 'SE4': 746.33, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T11:45:00Z', + 'deliveryStart': '2025-10-01T11:30:00Z', + 'entryPerArea': dict({ + 'SE3': 679.14, + 'SE4': 766.45, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T12:00:00Z', + 'deliveryStart': '2025-10-01T11:45:00Z', + 'entryPerArea': dict({ + 'SE3': 694.61, + 'SE4': 782.91, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T12:15:00Z', + 'deliveryStart': '2025-10-01T12:00:00Z', + 'entryPerArea': dict({ + 'SE3': 622.33, + 'SE4': 708.87, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T12:30:00Z', + 'deliveryStart': '2025-10-01T12:15:00Z', + 'entryPerArea': dict({ + 'SE3': 685.44, + 'SE4': 775.84, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T12:45:00Z', + 'deliveryStart': '2025-10-01T12:30:00Z', + 'entryPerArea': dict({ + 'SE3': 732.85, + 'SE4': 826.57, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T13:00:00Z', + 'deliveryStart': '2025-10-01T12:45:00Z', + 'entryPerArea': dict({ + 'SE3': 801.92, + 'SE4': 901.28, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T13:15:00Z', + 'deliveryStart': '2025-10-01T13:00:00Z', + 'entryPerArea': dict({ + 'SE3': 629.4, + 'SE4': 717.93, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T13:30:00Z', + 'deliveryStart': '2025-10-01T13:15:00Z', + 'entryPerArea': dict({ + 'SE3': 729.53, + 'SE4': 825.46, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T13:45:00Z', + 'deliveryStart': '2025-10-01T13:30:00Z', + 'entryPerArea': dict({ + 'SE3': 884.81, + 'SE4': 983.95, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T14:00:00Z', + 'deliveryStart': '2025-10-01T13:45:00Z', + 'entryPerArea': dict({ + 'SE3': 984.94, + 'SE4': 1089.71, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T14:15:00Z', + 'deliveryStart': '2025-10-01T14:00:00Z', + 'entryPerArea': dict({ + 'SE3': 615.26, + 'SE4': 703.12, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T14:30:00Z', + 'deliveryStart': '2025-10-01T14:15:00Z', + 'entryPerArea': dict({ + 'SE3': 902.94, + 'SE4': 1002.74, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T14:45:00Z', + 'deliveryStart': '2025-10-01T14:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1043.85, + 'SE4': 1158.35, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T15:00:00Z', + 'deliveryStart': '2025-10-01T14:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1075.12, + 'SE4': 1194.15, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T15:15:00Z', + 'deliveryStart': '2025-10-01T15:00:00Z', + 'entryPerArea': dict({ + 'SE3': 980.52, + 'SE4': 1089.38, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T15:30:00Z', + 'deliveryStart': '2025-10-01T15:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1162.66, + 'SE4': 1300.14, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T15:45:00Z', + 'deliveryStart': '2025-10-01T15:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1453.87, + 'SE4': 1628.6, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T16:00:00Z', + 'deliveryStart': '2025-10-01T15:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1955.96, + 'SE4': 2193.35, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T16:15:00Z', + 'deliveryStart': '2025-10-01T16:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1423.48, + 'SE4': 1623.74, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T16:30:00Z', + 'deliveryStart': '2025-10-01T16:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1900.04, + 'SE4': 2199.98, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T16:45:00Z', + 'deliveryStart': '2025-10-01T16:30:00Z', + 'entryPerArea': dict({ + 'SE3': 2611.11, + 'SE4': 3031.08, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T17:00:00Z', + 'deliveryStart': '2025-10-01T16:45:00Z', + 'entryPerArea': dict({ + 'SE3': 3467.41, + 'SE4': 4029.51, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T17:15:00Z', + 'deliveryStart': '2025-10-01T17:00:00Z', + 'entryPerArea': dict({ + 'SE3': 3828.03, + 'SE4': 4442.74, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T17:30:00Z', + 'deliveryStart': '2025-10-01T17:15:00Z', + 'entryPerArea': dict({ + 'SE3': 3429.83, + 'SE4': 3982.21, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T17:45:00Z', + 'deliveryStart': '2025-10-01T17:30:00Z', + 'entryPerArea': dict({ + 'SE3': 2934.38, + 'SE4': 3405.74, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T18:00:00Z', + 'deliveryStart': '2025-10-01T17:45:00Z', + 'entryPerArea': dict({ + 'SE3': 2308.07, + 'SE4': 2677.64, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T18:15:00Z', + 'deliveryStart': '2025-10-01T18:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1997.96, 'SE4': 0.0, }), }), dict({ - 'deliveryEnd': '2024-11-05T20:00:00Z', - 'deliveryStart': '2024-11-05T19:00:00Z', + 'deliveryEnd': '2025-10-01T18:30:00Z', + 'deliveryStart': '2025-10-01T18:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1424.03, + 'SE4': 1646.17, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T18:45:00Z', + 'deliveryStart': '2025-10-01T18:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1216.81, + 'SE4': 1388.11, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T19:00:00Z', + 'deliveryStart': '2025-10-01T18:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1070.15, + 'SE4': 1204.65, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T19:15:00Z', + 'deliveryStart': '2025-10-01T19:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1218.14, + 'SE4': 1405.02, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T19:30:00Z', + 'deliveryStart': '2025-10-01T19:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1135.8, + 'SE4': 1309.42, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T19:45:00Z', + 'deliveryStart': '2025-10-01T19:30:00Z', + 'entryPerArea': dict({ + 'SE3': 959.96, + 'SE4': 1115.69, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T20:00:00Z', + 'deliveryStart': '2025-10-01T19:45:00Z', + 'entryPerArea': dict({ + 'SE3': 913.66, + 'SE4': 1064.52, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T20:15:00Z', + 'deliveryStart': '2025-10-01T20:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1001.63, + 'SE4': 1161.22, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T20:30:00Z', + 'deliveryStart': '2025-10-01T20:15:00Z', + 'entryPerArea': dict({ + 'SE3': 933.0, + 'SE4': 1083.08, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T20:45:00Z', + 'deliveryStart': '2025-10-01T20:30:00Z', + 'entryPerArea': dict({ + 'SE3': 874.53, + 'SE4': 1017.66, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T21:00:00Z', + 'deliveryStart': '2025-10-01T20:45:00Z', + 'entryPerArea': dict({ + 'SE3': 821.71, + 'SE4': 955.32, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T21:15:00Z', + 'deliveryStart': '2025-10-01T21:00:00Z', + 'entryPerArea': dict({ + 'SE3': 860.5, + 'SE4': 997.32, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T21:30:00Z', + 'deliveryStart': '2025-10-01T21:15:00Z', + 'entryPerArea': dict({ + 'SE3': 840.16, + 'SE4': 977.87, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T21:45:00Z', + 'deliveryStart': '2025-10-01T21:30:00Z', + 'entryPerArea': dict({ + 'SE3': 820.05, + 'SE4': 954.66, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T22:00:00Z', + 'deliveryStart': '2025-10-01T21:45:00Z', + 'entryPerArea': dict({ + 'SE3': 785.68, + 'SE4': 912.22, + }), + }), + ]), + 'updatedAt': '2025-09-30T12:08:16.4448023Z', + 'version': 3, + }), + '2025-10-02': dict({ + 'areaAverages': list([ + dict({ + 'areaCode': 'SE3', + 'price': 1129.65, + }), + dict({ + 'areaCode': 'SE4', + 'price': 1119.28, + }), + ]), + 'areaStates': list([ + dict({ + 'areas': list([ + 'SE3', + 'SE4', + ]), + 'state': 'Final', + }), + ]), + 'blockPriceAggregates': list([ + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 961.76, + 'max': 1831.25, + 'min': 673.05, + }), + 'SE4': dict({ + 'average': 1102.25, + 'max': 2182.34, + 'min': 758.78, + }), + }), + 'blockName': 'Off-peak 1', + 'deliveryEnd': '2025-10-02T06:00:00Z', + 'deliveryStart': '2025-10-01T22:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 1191.34, + 'max': 3288.35, + 'min': 563.38, + }), + 'SE4': dict({ + 'average': 1155.07, + 'max': 2617.73, + 'min': 635.76, + }), + }), + 'blockName': 'Peak', + 'deliveryEnd': '2025-10-02T18:00:00Z', + 'deliveryStart': '2025-10-02T06:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 1280.38, + 'max': 1935.08, + 'min': 646.9, + }), + 'SE4': dict({ + 'average': 1045.99, + 'max': 1532.57, + 'min': 591.84, + }), + }), + 'blockName': 'Off-peak 2', + 'deliveryEnd': '2025-10-02T22:00:00Z', + 'deliveryStart': '2025-10-02T18:00:00Z', + }), + ]), + 'currency': 'SEK', + 'deliveryAreas': list([ + 'SE3', + 'SE4', + ]), + 'deliveryDateCET': '2025-10-02', + 'exchangeRate': 11.03362, + 'market': 'DayAhead', + 'multiAreaEntries': list([ + dict({ + 'deliveryEnd': '2025-10-01T22:15:00Z', + 'deliveryStart': '2025-10-01T22:00:00Z', + 'entryPerArea': dict({ + 'SE3': 933.22, + 'SE4': 1062.32, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T22:30:00Z', + 'deliveryStart': '2025-10-01T22:15:00Z', + 'entryPerArea': dict({ + 'SE3': 854.22, + 'SE4': 971.95, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T22:45:00Z', + 'deliveryStart': '2025-10-01T22:30:00Z', + 'entryPerArea': dict({ + 'SE3': 809.54, + 'SE4': 919.1, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T23:00:00Z', + 'deliveryStart': '2025-10-01T22:45:00Z', + 'entryPerArea': dict({ + 'SE3': 811.74, + 'SE4': 922.63, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T23:15:00Z', + 'deliveryStart': '2025-10-01T23:00:00Z', + 'entryPerArea': dict({ + 'SE3': 835.13, + 'SE4': 950.99, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T23:30:00Z', + 'deliveryStart': '2025-10-01T23:15:00Z', + 'entryPerArea': dict({ + 'SE3': 828.85, + 'SE4': 942.82, + }), + }), + dict({ + 'deliveryEnd': '2025-10-01T23:45:00Z', + 'deliveryStart': '2025-10-01T23:30:00Z', + 'entryPerArea': dict({ + 'SE3': 796.63, + 'SE4': 903.54, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T00:00:00Z', + 'deliveryStart': '2025-10-01T23:45:00Z', + 'entryPerArea': dict({ + 'SE3': 706.7, + 'SE4': 799.61, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T00:15:00Z', + 'deliveryStart': '2025-10-02T00:00:00Z', + 'entryPerArea': dict({ + 'SE3': 695.23, + 'SE4': 786.81, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T00:30:00Z', + 'deliveryStart': '2025-10-02T00:15:00Z', + 'entryPerArea': dict({ + 'SE3': 695.12, + 'SE4': 783.83, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T00:45:00Z', + 'deliveryStart': '2025-10-02T00:30:00Z', + 'entryPerArea': dict({ + 'SE3': 684.86, + 'SE4': 771.8, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T01:00:00Z', + 'deliveryStart': '2025-10-02T00:45:00Z', 'entryPerArea': dict({ - 'SE3': 835.53, - 'SE4': 1112.57, + 'SE3': 673.05, + 'SE4': 758.78, }), }), dict({ - 'deliveryEnd': '2024-11-05T21:00:00Z', - 'deliveryStart': '2024-11-05T20:00:00Z', + 'deliveryEnd': '2025-10-02T01:15:00Z', + 'deliveryStart': '2025-10-02T01:00:00Z', 'entryPerArea': dict({ - 'SE3': 796.19, - 'SE4': 1051.69, + 'SE3': 695.01, + 'SE4': 791.22, }), }), dict({ - 'deliveryEnd': '2024-11-05T22:00:00Z', - 'deliveryStart': '2024-11-05T21:00:00Z', + 'deliveryEnd': '2025-10-02T01:30:00Z', + 'deliveryStart': '2025-10-02T01:15:00Z', 'entryPerArea': dict({ - 'SE3': 522.3, - 'SE4': 662.44, + 'SE3': 693.35, + 'SE4': 789.12, }), }), dict({ - 'deliveryEnd': '2024-11-05T23:00:00Z', - 'deliveryStart': '2024-11-05T22:00:00Z', + 'deliveryEnd': '2025-10-02T01:45:00Z', + 'deliveryStart': '2025-10-02T01:30:00Z', 'entryPerArea': dict({ - 'SE3': 289.14, - 'SE4': 349.21, + 'SE3': 702.4, + 'SE4': 799.61, }), }), - ]), - 'updatedAt': '2024-11-04T12:15:03.9456464Z', - 'version': 3, - }), - '2024-11-06': dict({ - 'areaAverages': list([ dict({ - 'areaCode': 'SE3', - 'price': 900.65, + 'deliveryEnd': '2025-10-02T02:00:00Z', + 'deliveryStart': '2025-10-02T01:45:00Z', + 'entryPerArea': dict({ + 'SE3': 749.4, + 'SE4': 853.45, + }), }), dict({ - 'areaCode': 'SE4', - 'price': 1581.19, + 'deliveryEnd': '2025-10-02T02:15:00Z', + 'deliveryStart': '2025-10-02T02:00:00Z', + 'entryPerArea': dict({ + 'SE3': 796.85, + 'SE4': 907.4, + }), }), - ]), - 'areaStates': list([ dict({ - 'areas': list([ - 'SE3', - 'SE4', - ]), - 'state': 'Final', + 'deliveryEnd': '2025-10-02T02:30:00Z', + 'deliveryStart': '2025-10-02T02:15:00Z', + 'entryPerArea': dict({ + 'SE3': 811.19, + 'SE4': 924.07, + }), }), - ]), - 'blockPriceAggregates': list([ dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 422.51, - 'max': 1820.5, - 'min': 74.06, - }), - 'SE4': dict({ - 'average': 706.61, - 'max': 2449.96, - 'min': 157.34, - }), + 'deliveryEnd': '2025-10-02T02:45:00Z', + 'deliveryStart': '2025-10-02T02:30:00Z', + 'entryPerArea': dict({ + 'SE3': 803.8, + 'SE4': 916.23, }), - 'blockName': 'Off-peak 1', - 'deliveryEnd': '2024-11-06T07:00:00Z', - 'deliveryStart': '2024-11-05T23:00:00Z', }), dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 1346.82, - 'max': 2366.57, - 'min': 903.31, - }), - 'SE4': dict({ - 'average': 2306.88, - 'max': 5511.77, - 'min': 1362.84, - }), + 'deliveryEnd': '2025-10-02T03:00:00Z', + 'deliveryStart': '2025-10-02T02:45:00Z', + 'entryPerArea': dict({ + 'SE3': 839.11, + 'SE4': 953.3, }), - 'blockName': 'Peak', - 'deliveryEnd': '2024-11-06T19:00:00Z', - 'deliveryStart': '2024-11-06T07:00:00Z', }), dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 518.43, - 'max': 716.82, - 'min': 250.64, - }), - 'SE4': dict({ - 'average': 1153.25, - 'max': 1624.33, - 'min': 539.42, - }), + 'deliveryEnd': '2025-10-02T03:15:00Z', + 'deliveryStart': '2025-10-02T03:00:00Z', + 'entryPerArea': dict({ + 'SE3': 825.2, + 'SE4': 943.15, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T03:30:00Z', + 'deliveryStart': '2025-10-02T03:15:00Z', + 'entryPerArea': dict({ + 'SE3': 838.78, + 'SE4': 958.93, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T03:45:00Z', + 'deliveryStart': '2025-10-02T03:30:00Z', + 'entryPerArea': dict({ + 'SE3': 906.19, + 'SE4': 1030.65, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T04:00:00Z', + 'deliveryStart': '2025-10-02T03:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1057.79, + 'SE4': 1195.82, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T04:15:00Z', + 'deliveryStart': '2025-10-02T04:00:00Z', + 'entryPerArea': dict({ + 'SE3': 912.15, + 'SE4': 1040.8, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T04:30:00Z', + 'deliveryStart': '2025-10-02T04:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1131.28, + 'SE4': 1283.43, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T04:45:00Z', + 'deliveryStart': '2025-10-02T04:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1294.68, + 'SE4': 1468.91, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T05:00:00Z', + 'deliveryStart': '2025-10-02T04:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1625.8, + 'SE4': 1845.81, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T05:15:00Z', + 'deliveryStart': '2025-10-02T05:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1649.31, + 'SE4': 1946.77, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T05:30:00Z', + 'deliveryStart': '2025-10-02T05:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1831.25, + 'SE4': 2182.34, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T05:45:00Z', + 'deliveryStart': '2025-10-02T05:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1743.31, + 'SE4': 2063.4, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T06:00:00Z', + 'deliveryStart': '2025-10-02T05:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1545.04, + 'SE4': 1803.33, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T06:15:00Z', + 'deliveryStart': '2025-10-02T06:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1783.47, + 'SE4': 2080.72, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T06:30:00Z', + 'deliveryStart': '2025-10-02T06:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1470.89, + 'SE4': 1675.23, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T06:45:00Z', + 'deliveryStart': '2025-10-02T06:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1191.08, + 'SE4': 1288.06, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T07:00:00Z', + 'deliveryStart': '2025-10-02T06:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1012.22, + 'SE4': 1112.19, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T07:15:00Z', + 'deliveryStart': '2025-10-02T07:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1278.69, + 'SE4': 1375.67, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T07:30:00Z', + 'deliveryStart': '2025-10-02T07:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1170.12, + 'SE4': 1258.61, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T07:45:00Z', + 'deliveryStart': '2025-10-02T07:30:00Z', + 'entryPerArea': dict({ + 'SE3': 937.09, + 'SE4': 1021.93, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T08:00:00Z', + 'deliveryStart': '2025-10-02T07:45:00Z', + 'entryPerArea': dict({ + 'SE3': 815.94, + 'SE4': 900.67, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T08:15:00Z', + 'deliveryStart': '2025-10-02T08:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1044.66, + 'SE4': 1135.25, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T08:30:00Z', + 'deliveryStart': '2025-10-02T08:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1020.61, + 'SE4': 1112.74, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T08:45:00Z', + 'deliveryStart': '2025-10-02T08:30:00Z', + 'entryPerArea': dict({ + 'SE3': 866.14, + 'SE4': 953.53, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T09:00:00Z', + 'deliveryStart': '2025-10-02T08:45:00Z', + 'entryPerArea': dict({ + 'SE3': 774.34, + 'SE4': 860.18, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T09:15:00Z', + 'deliveryStart': '2025-10-02T09:00:00Z', + 'entryPerArea': dict({ + 'SE3': 928.26, + 'SE4': 1020.39, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T09:30:00Z', + 'deliveryStart': '2025-10-02T09:15:00Z', + 'entryPerArea': dict({ + 'SE3': 834.47, + 'SE4': 922.96, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T09:45:00Z', + 'deliveryStart': '2025-10-02T09:30:00Z', + 'entryPerArea': dict({ + 'SE3': 712.33, + 'SE4': 794.64, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T10:00:00Z', + 'deliveryStart': '2025-10-02T09:45:00Z', + 'entryPerArea': dict({ + 'SE3': 646.46, + 'SE4': 725.9, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T10:15:00Z', + 'deliveryStart': '2025-10-02T10:00:00Z', + 'entryPerArea': dict({ + 'SE3': 692.91, + 'SE4': 773.9, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T10:30:00Z', + 'deliveryStart': '2025-10-02T10:15:00Z', + 'entryPerArea': dict({ + 'SE3': 627.59, + 'SE4': 706.59, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T10:45:00Z', + 'deliveryStart': '2025-10-02T10:30:00Z', + 'entryPerArea': dict({ + 'SE3': 630.02, + 'SE4': 708.14, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T11:00:00Z', + 'deliveryStart': '2025-10-02T10:45:00Z', + 'entryPerArea': dict({ + 'SE3': 625.94, + 'SE4': 703.61, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T11:15:00Z', + 'deliveryStart': '2025-10-02T11:00:00Z', + 'entryPerArea': dict({ + 'SE3': 563.38, + 'SE4': 635.76, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T11:30:00Z', + 'deliveryStart': '2025-10-02T11:15:00Z', + 'entryPerArea': dict({ + 'SE3': 588.42, + 'SE4': 663.12, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T11:45:00Z', + 'deliveryStart': '2025-10-02T11:30:00Z', + 'entryPerArea': dict({ + 'SE3': 597.03, + 'SE4': 672.83, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T12:00:00Z', + 'deliveryStart': '2025-10-02T11:45:00Z', + 'entryPerArea': dict({ + 'SE3': 608.61, + 'SE4': 685.19, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T12:15:00Z', + 'deliveryStart': '2025-10-02T12:00:00Z', + 'entryPerArea': dict({ + 'SE3': 599.24, + 'SE4': 676.91, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T12:30:00Z', + 'deliveryStart': '2025-10-02T12:15:00Z', + 'entryPerArea': dict({ + 'SE3': 649.77, + 'SE4': 729.54, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T12:45:00Z', + 'deliveryStart': '2025-10-02T12:30:00Z', + 'entryPerArea': dict({ + 'SE3': 728.22, + 'SE4': 821.23, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T13:00:00Z', + 'deliveryStart': '2025-10-02T12:45:00Z', + 'entryPerArea': dict({ + 'SE3': 803.91, + 'SE4': 909.06, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T13:15:00Z', + 'deliveryStart': '2025-10-02T13:00:00Z', + 'entryPerArea': dict({ + 'SE3': 594.38, + 'SE4': 679.23, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T13:30:00Z', + 'deliveryStart': '2025-10-02T13:15:00Z', + 'entryPerArea': dict({ + 'SE3': 738.48, + 'SE4': 825.09, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T13:45:00Z', + 'deliveryStart': '2025-10-02T13:30:00Z', + 'entryPerArea': dict({ + 'SE3': 873.53, + 'SE4': 962.02, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T14:00:00Z', + 'deliveryStart': '2025-10-02T13:45:00Z', + 'entryPerArea': dict({ + 'SE3': 994.57, + 'SE4': 1083.5, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T14:15:00Z', + 'deliveryStart': '2025-10-02T14:00:00Z', + 'entryPerArea': dict({ + 'SE3': 733.52, + 'SE4': 813.18, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T14:30:00Z', + 'deliveryStart': '2025-10-02T14:15:00Z', + 'entryPerArea': dict({ + 'SE3': 864.59, + 'SE4': 944.04, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T14:45:00Z', + 'deliveryStart': '2025-10-02T14:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1032.08, + 'SE4': 1113.18, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T15:00:00Z', + 'deliveryStart': '2025-10-02T14:45:00Z', + 'entryPerArea': dict({ + 'SE3': 1153.01, + 'SE4': 1241.61, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T15:15:00Z', + 'deliveryStart': '2025-10-02T15:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1271.18, + 'SE4': 1017.41, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T15:30:00Z', + 'deliveryStart': '2025-10-02T15:15:00Z', + 'entryPerArea': dict({ + 'SE3': 1375.23, + 'SE4': 1093.1, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T15:45:00Z', + 'deliveryStart': '2025-10-02T15:30:00Z', + 'entryPerArea': dict({ + 'SE3': 1544.82, + 'SE4': 1244.81, + }), + }), + dict({ + 'deliveryEnd': '2025-10-02T16:00:00Z', + 'deliveryStart': '2025-10-02T15:45:00Z', + 'entryPerArea': dict({ + 'SE3': 2412.17, + 'SE4': 1960.12, }), - 'blockName': 'Off-peak 2', - 'deliveryEnd': '2024-11-06T23:00:00Z', - 'deliveryStart': '2024-11-06T19:00:00Z', }), - ]), - 'currency': 'SEK', - 'deliveryAreas': list([ - 'SE3', - 'SE4', - ]), - 'deliveryDateCET': '2024-11-06', - 'exchangeRate': 11.66314, - 'market': 'DayAhead', - 'multiAreaEntries': list([ dict({ - 'deliveryEnd': '2024-11-06T00:00:00Z', - 'deliveryStart': '2024-11-05T23:00:00Z', + 'deliveryEnd': '2025-10-02T16:15:00Z', + 'deliveryStart': '2025-10-02T16:00:00Z', 'entryPerArea': dict({ - 'SE3': 126.66, - 'SE4': 275.6, + 'SE3': 1677.66, + 'SE4': 1334.3, }), }), dict({ - 'deliveryEnd': '2024-11-06T01:00:00Z', - 'deliveryStart': '2024-11-06T00:00:00Z', + 'deliveryEnd': '2025-10-02T16:30:00Z', + 'deliveryStart': '2025-10-02T16:15:00Z', 'entryPerArea': dict({ - 'SE3': 74.06, - 'SE4': 157.34, + 'SE3': 2010.55, + 'SE4': 1606.61, }), }), dict({ - 'deliveryEnd': '2024-11-06T02:00:00Z', - 'deliveryStart': '2024-11-06T01:00:00Z', + 'deliveryEnd': '2025-10-02T16:45:00Z', + 'deliveryStart': '2025-10-02T16:30:00Z', 'entryPerArea': dict({ - 'SE3': 78.38, - 'SE4': 165.62, + 'SE3': 2524.38, + 'SE4': 2013.53, }), }), dict({ - 'deliveryEnd': '2024-11-06T03:00:00Z', - 'deliveryStart': '2024-11-06T02:00:00Z', + 'deliveryEnd': '2025-10-02T17:00:00Z', + 'deliveryStart': '2025-10-02T16:45:00Z', 'entryPerArea': dict({ - 'SE3': 92.37, - 'SE4': 196.17, + 'SE3': 3288.35, + 'SE4': 2617.73, }), }), dict({ - 'deliveryEnd': '2024-11-06T04:00:00Z', - 'deliveryStart': '2024-11-06T03:00:00Z', + 'deliveryEnd': '2025-10-02T17:15:00Z', + 'deliveryStart': '2025-10-02T17:00:00Z', 'entryPerArea': dict({ - 'SE3': 99.14, - 'SE4': 190.58, + 'SE3': 3065.69, + 'SE4': 2472.19, }), }), dict({ - 'deliveryEnd': '2024-11-06T05:00:00Z', - 'deliveryStart': '2024-11-06T04:00:00Z', + 'deliveryEnd': '2025-10-02T17:30:00Z', + 'deliveryStart': '2025-10-02T17:15:00Z', 'entryPerArea': dict({ - 'SE3': 447.51, - 'SE4': 932.93, + 'SE3': 2824.72, + 'SE4': 2276.46, }), }), dict({ - 'deliveryEnd': '2024-11-06T06:00:00Z', - 'deliveryStart': '2024-11-06T05:00:00Z', + 'deliveryEnd': '2025-10-02T17:45:00Z', + 'deliveryStart': '2025-10-02T17:30:00Z', 'entryPerArea': dict({ - 'SE3': 641.47, - 'SE4': 1284.69, + 'SE3': 2279.66, + 'SE4': 1835.44, }), }), dict({ - 'deliveryEnd': '2024-11-06T07:00:00Z', - 'deliveryStart': '2024-11-06T06:00:00Z', + 'deliveryEnd': '2025-10-02T18:00:00Z', + 'deliveryStart': '2025-10-02T17:45:00Z', 'entryPerArea': dict({ - 'SE3': 1820.5, - 'SE4': 2449.96, + 'SE3': 1723.78, + 'SE4': 1385.38, }), }), dict({ - 'deliveryEnd': '2024-11-06T08:00:00Z', - 'deliveryStart': '2024-11-06T07:00:00Z', + 'deliveryEnd': '2025-10-02T18:15:00Z', + 'deliveryStart': '2025-10-02T18:00:00Z', 'entryPerArea': dict({ - 'SE3': 1723.0, - 'SE4': 2244.22, + 'SE3': 1935.08, + 'SE4': 1532.57, }), }), dict({ - 'deliveryEnd': '2024-11-06T09:00:00Z', - 'deliveryStart': '2024-11-06T08:00:00Z', + 'deliveryEnd': '2025-10-02T18:30:00Z', + 'deliveryStart': '2025-10-02T18:15:00Z', 'entryPerArea': dict({ - 'SE3': 1298.57, - 'SE4': 1643.45, + 'SE3': 1568.54, + 'SE4': 1240.18, }), }), dict({ - 'deliveryEnd': '2024-11-06T10:00:00Z', - 'deliveryStart': '2024-11-06T09:00:00Z', + 'deliveryEnd': '2025-10-02T18:45:00Z', + 'deliveryStart': '2025-10-02T18:30:00Z', 'entryPerArea': dict({ - 'SE3': 1099.25, - 'SE4': 1507.23, + 'SE3': 1430.51, + 'SE4': 1115.61, }), }), dict({ - 'deliveryEnd': '2024-11-06T11:00:00Z', - 'deliveryStart': '2024-11-06T10:00:00Z', + 'deliveryEnd': '2025-10-02T19:00:00Z', + 'deliveryStart': '2025-10-02T18:45:00Z', 'entryPerArea': dict({ - 'SE3': 903.31, - 'SE4': 1362.84, + 'SE3': 1377.66, + 'SE4': 1075.12, }), }), dict({ - 'deliveryEnd': '2024-11-06T12:00:00Z', - 'deliveryStart': '2024-11-06T11:00:00Z', + 'deliveryEnd': '2025-10-02T19:15:00Z', + 'deliveryStart': '2025-10-02T19:00:00Z', 'entryPerArea': dict({ - 'SE3': 959.99, - 'SE4': 1376.13, + 'SE3': 1408.44, + 'SE4': 1108.66, }), }), dict({ - 'deliveryEnd': '2024-11-06T13:00:00Z', - 'deliveryStart': '2024-11-06T12:00:00Z', + 'deliveryEnd': '2025-10-02T19:30:00Z', + 'deliveryStart': '2025-10-02T19:15:00Z', 'entryPerArea': dict({ - 'SE3': 1186.61, - 'SE4': 1449.96, + 'SE3': 1326.79, + 'SE4': 1049.74, }), }), dict({ - 'deliveryEnd': '2024-11-06T14:00:00Z', - 'deliveryStart': '2024-11-06T13:00:00Z', + 'deliveryEnd': '2025-10-02T19:45:00Z', + 'deliveryStart': '2025-10-02T19:30:00Z', 'entryPerArea': dict({ - 'SE3': 1307.67, - 'SE4': 1608.35, + 'SE3': 1210.94, + 'SE4': 951.1, }), }), dict({ - 'deliveryEnd': '2024-11-06T15:00:00Z', - 'deliveryStart': '2024-11-06T14:00:00Z', + 'deliveryEnd': '2025-10-02T20:00:00Z', + 'deliveryStart': '2025-10-02T19:45:00Z', 'entryPerArea': dict({ - 'SE3': 1385.46, - 'SE4': 2110.8, + 'SE3': 1293.58, + 'SE4': 1026.79, }), }), dict({ - 'deliveryEnd': '2024-11-06T16:00:00Z', - 'deliveryStart': '2024-11-06T15:00:00Z', + 'deliveryEnd': '2025-10-02T20:15:00Z', + 'deliveryStart': '2025-10-02T20:00:00Z', 'entryPerArea': dict({ - 'SE3': 1366.8, - 'SE4': 3031.25, + 'SE3': 1385.71, + 'SE4': 1091.0, }), }), dict({ - 'deliveryEnd': '2024-11-06T17:00:00Z', - 'deliveryStart': '2024-11-06T16:00:00Z', + 'deliveryEnd': '2025-10-02T20:30:00Z', + 'deliveryStart': '2025-10-02T20:15:00Z', 'entryPerArea': dict({ - 'SE3': 2366.57, - 'SE4': 5511.77, + 'SE3': 1341.47, + 'SE4': 1104.13, }), }), dict({ - 'deliveryEnd': '2024-11-06T18:00:00Z', - 'deliveryStart': '2024-11-06T17:00:00Z', + 'deliveryEnd': '2025-10-02T20:45:00Z', + 'deliveryStart': '2025-10-02T20:30:00Z', 'entryPerArea': dict({ - 'SE3': 1481.92, - 'SE4': 3351.64, + 'SE3': 1284.98, + 'SE4': 1024.36, }), }), dict({ - 'deliveryEnd': '2024-11-06T19:00:00Z', - 'deliveryStart': '2024-11-06T18:00:00Z', + 'deliveryEnd': '2025-10-02T21:00:00Z', + 'deliveryStart': '2025-10-02T20:45:00Z', 'entryPerArea': dict({ - 'SE3': 1082.69, - 'SE4': 2484.95, + 'SE3': 1071.47, + 'SE4': 892.51, }), }), dict({ - 'deliveryEnd': '2024-11-06T20:00:00Z', - 'deliveryStart': '2024-11-06T19:00:00Z', + 'deliveryEnd': '2025-10-02T21:15:00Z', + 'deliveryStart': '2025-10-02T21:00:00Z', 'entryPerArea': dict({ - 'SE3': 716.82, - 'SE4': 1624.33, + 'SE3': 1218.0, + 'SE4': 1123.99, }), }), dict({ - 'deliveryEnd': '2024-11-06T21:00:00Z', - 'deliveryStart': '2024-11-06T20:00:00Z', + 'deliveryEnd': '2025-10-02T21:30:00Z', + 'deliveryStart': '2025-10-02T21:15:00Z', 'entryPerArea': dict({ - 'SE3': 583.16, - 'SE4': 1306.27, + 'SE3': 1112.3, + 'SE4': 1001.63, }), }), dict({ - 'deliveryEnd': '2024-11-06T22:00:00Z', - 'deliveryStart': '2024-11-06T21:00:00Z', + 'deliveryEnd': '2025-10-02T21:45:00Z', + 'deliveryStart': '2025-10-02T21:30:00Z', 'entryPerArea': dict({ - 'SE3': 523.09, - 'SE4': 1142.99, + 'SE3': 873.64, + 'SE4': 806.67, }), }), dict({ - 'deliveryEnd': '2024-11-06T23:00:00Z', - 'deliveryStart': '2024-11-06T22:00:00Z', + 'deliveryEnd': '2025-10-02T22:00:00Z', + 'deliveryStart': '2025-10-02T21:45:00Z', 'entryPerArea': dict({ - 'SE3': 250.64, - 'SE4': 539.42, + 'SE3': 646.9, + 'SE4': 591.84, }), }), ]), - 'updatedAt': '2024-11-05T12:12:51.9853434Z', + 'updatedAt': '2025-10-01T11:25:06.1484362Z', 'version': 3, }), }), diff --git a/tests/components/nordpool/snapshots/test_sensor.ambr b/tests/components/nordpool/snapshots/test_sensor.ambr index 232836d1cc9a3f..b2a53981fbc326 100644 --- a/tests/components/nordpool/snapshots/test_sensor.ambr +++ b/tests/components/nordpool/snapshots/test_sensor.ambr @@ -99,7 +99,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.01177', + 'state': '1.99796', }) # --- # name: test_sensor[sensor.nord_pool_se3_daily_average-entry] @@ -154,7 +154,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.90074', + 'state': '1.03398', }) # --- # name: test_sensor[sensor.nord_pool_se3_exchange_rate-entry] @@ -205,7 +205,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '11.6402', + 'state': '11.05186', }) # --- # name: test_sensor[sensor.nord_pool_se3_highest_price-entry] @@ -249,9 +249,9 @@ # name: test_sensor[sensor.nord_pool_se3_highest_price-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'end': '2024-11-05T17:00:00+00:00', + 'end': '2025-10-01T17:15:00+00:00', 'friendly_name': 'Nord Pool SE3 Highest price', - 'start': '2024-11-05T16:00:00+00:00', + 'start': '2025-10-01T17:00:00+00:00', 'unit_of_measurement': 'SEK/kWh', }), 'context': , @@ -259,7 +259,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.51265', + 'state': '3.82803', }) # --- # name: test_sensor[sensor.nord_pool_se3_last_updated-entry] @@ -308,7 +308,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-04T12:15:03+00:00', + 'state': '2025-09-30T12:08:16+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se3_lowest_price-entry] @@ -352,9 +352,9 @@ # name: test_sensor[sensor.nord_pool_se3_lowest_price-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'end': '2024-11-05T03:00:00+00:00', + 'end': '2025-10-01T02:15:00+00:00', 'friendly_name': 'Nord Pool SE3 Lowest price', - 'start': '2024-11-05T02:00:00+00:00', + 'start': '2025-10-01T02:00:00+00:00', 'unit_of_measurement': 'SEK/kWh', }), 'context': , @@ -362,7 +362,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.06169', + 'state': '0.44196', }) # --- # name: test_sensor[sensor.nord_pool_se3_next_price-entry] @@ -414,7 +414,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.83553', + 'state': '1.21814', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_1_average-entry] @@ -469,7 +469,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.42287', + 'state': '0.74593', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_1_highest_price-entry] @@ -524,7 +524,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.40614', + 'state': '1.80996', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_1_lowest_price-entry] @@ -579,7 +579,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.06169', + 'state': '0.44196', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_1_time_from-entry] @@ -628,7 +628,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-04T23:00:00+00:00', + 'state': '2025-09-30T22:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_1_time_until-entry] @@ -677,7 +677,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T07:00:00+00:00', + 'state': '2025-10-01T06:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_2_average-entry] @@ -732,7 +732,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.61079', + 'state': '1.05461', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_2_highest_price-entry] @@ -787,7 +787,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.83553', + 'state': '1.99796', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_2_lowest_price-entry] @@ -842,7 +842,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.28914', + 'state': '0.78568', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_2_time_from-entry] @@ -891,7 +891,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T19:00:00+00:00', + 'state': '2025-10-01T18:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se3_off_peak_2_time_until-entry] @@ -940,7 +940,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T23:00:00+00:00', + 'state': '2025-10-01T22:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se3_peak_average-entry] @@ -995,7 +995,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.31597', + 'state': '1.21913', }) # --- # name: test_sensor[sensor.nord_pool_se3_peak_highest_price-entry] @@ -1050,7 +1050,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.51265', + 'state': '3.82803', }) # --- # name: test_sensor[sensor.nord_pool_se3_peak_lowest_price-entry] @@ -1105,7 +1105,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.92505', + 'state': '0.60774', }) # --- # name: test_sensor[sensor.nord_pool_se3_peak_time_from-entry] @@ -1154,7 +1154,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T07:00:00+00:00', + 'state': '2025-10-01T06:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se3_peak_time_until-entry] @@ -1203,7 +1203,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T19:00:00+00:00', + 'state': '2025-10-01T18:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se3_previous_price-entry] @@ -1255,7 +1255,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.81983', + 'state': '3.82803', }) # --- # name: test_sensor[sensor.nord_pool_se4_currency-entry] @@ -1413,7 +1413,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.16612', + 'state': '1.18078', }) # --- # name: test_sensor[sensor.nord_pool_se4_exchange_rate-entry] @@ -1464,7 +1464,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '11.6402', + 'state': '11.05186', }) # --- # name: test_sensor[sensor.nord_pool_se4_highest_price-entry] @@ -1508,9 +1508,9 @@ # name: test_sensor[sensor.nord_pool_se4_highest_price-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'end': '2024-11-05T17:00:00+00:00', + 'end': '2025-10-01T17:15:00+00:00', 'friendly_name': 'Nord Pool SE4 Highest price', - 'start': '2024-11-05T16:00:00+00:00', + 'start': '2025-10-01T17:00:00+00:00', 'unit_of_measurement': 'SEK/kWh', }), 'context': , @@ -1518,7 +1518,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3.53303', + 'state': '4.44274', }) # --- # name: test_sensor[sensor.nord_pool_se4_last_updated-entry] @@ -1567,7 +1567,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-04T12:15:03+00:00', + 'state': '2025-09-30T12:08:16+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se4_lowest_price-entry] @@ -1611,9 +1611,9 @@ # name: test_sensor[sensor.nord_pool_se4_lowest_price-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'end': '2024-11-05T19:00:00+00:00', + 'end': '2025-10-01T18:15:00+00:00', 'friendly_name': 'Nord Pool SE4 Lowest price', - 'start': '2024-11-05T18:00:00+00:00', + 'start': '2025-10-01T18:00:00+00:00', 'unit_of_measurement': 'SEK/kWh', }), 'context': , @@ -1673,7 +1673,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.11257', + 'state': '1.40502', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_1_average-entry] @@ -1728,7 +1728,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.49797', + 'state': '0.86099', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_1_highest_price-entry] @@ -1783,7 +1783,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.64825', + 'state': '2.02934', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_1_lowest_price-entry] @@ -1838,7 +1838,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.06519', + 'state': '0.51546', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_1_time_from-entry] @@ -1887,7 +1887,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-04T23:00:00+00:00', + 'state': '2025-09-30T22:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_1_time_until-entry] @@ -1936,7 +1936,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T07:00:00+00:00', + 'state': '2025-10-01T06:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_2_average-entry] @@ -1991,7 +1991,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.79398', + 'state': '1.21907', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_2_highest_price-entry] @@ -2046,7 +2046,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.11257', + 'state': '2.31216', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_2_lowest_price-entry] @@ -2101,7 +2101,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.34921', + 'state': '0.91222', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_2_time_from-entry] @@ -2150,7 +2150,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T19:00:00+00:00', + 'state': '2025-10-01T18:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se4_off_peak_2_time_until-entry] @@ -2199,7 +2199,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T23:00:00+00:00', + 'state': '2025-10-01T22:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se4_peak_average-entry] @@ -2254,7 +2254,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.73559', + 'state': '1.38122', }) # --- # name: test_sensor[sensor.nord_pool_se4_peak_highest_price-entry] @@ -2309,7 +2309,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3.53303', + 'state': '4.44274', }) # --- # name: test_sensor[sensor.nord_pool_se4_peak_lowest_price-entry] @@ -2364,7 +2364,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.08172', + 'state': '0.68312', }) # --- # name: test_sensor[sensor.nord_pool_se4_peak_time_from-entry] @@ -2413,7 +2413,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T07:00:00+00:00', + 'state': '2025-10-01T06:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se4_peak_time_until-entry] @@ -2462,7 +2462,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2024-11-05T19:00:00+00:00', + 'state': '2025-10-01T18:00:00+00:00', }) # --- # name: test_sensor[sensor.nord_pool_se4_previous_price-entry] @@ -2514,6 +2514,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.52406', + 'state': '4.44274', }) # --- diff --git a/tests/components/nordpool/snapshots/test_services.ambr b/tests/components/nordpool/snapshots/test_services.ambr index 5e39082f6474a8..a478791bd9a59c 100644 --- a/tests/components/nordpool/snapshots/test_services.ambr +++ b/tests/components/nordpool/snapshots/test_services.ambr @@ -9,124 +9,484 @@ dict({ 'SE3': list([ dict({ - 'end': '2024-11-05T00:00:00+00:00', - 'price': 250.73, - 'start': '2024-11-04T23:00:00+00:00', + 'end': '2025-09-30T22:15:00+00:00', + 'price': 556.68, + 'start': '2025-09-30T22:00:00+00:00', }), dict({ - 'end': '2024-11-05T01:00:00+00:00', - 'price': 76.36, - 'start': '2024-11-05T00:00:00+00:00', + 'end': '2025-09-30T22:30:00+00:00', + 'price': 519.88, + 'start': '2025-09-30T22:15:00+00:00', }), dict({ - 'end': '2024-11-05T02:00:00+00:00', - 'price': 73.92, - 'start': '2024-11-05T01:00:00+00:00', + 'end': '2025-09-30T22:45:00+00:00', + 'price': 508.28, + 'start': '2025-09-30T22:30:00+00:00', }), dict({ - 'end': '2024-11-05T03:00:00+00:00', - 'price': 61.69, - 'start': '2024-11-05T02:00:00+00:00', + 'end': '2025-09-30T23:00:00+00:00', + 'price': 509.93, + 'start': '2025-09-30T22:45:00+00:00', }), dict({ - 'end': '2024-11-05T04:00:00+00:00', - 'price': 64.6, - 'start': '2024-11-05T03:00:00+00:00', + 'end': '2025-09-30T23:15:00+00:00', + 'price': 501.64, + 'start': '2025-09-30T23:00:00+00:00', }), dict({ - 'end': '2024-11-05T05:00:00+00:00', - 'price': 453.27, - 'start': '2024-11-05T04:00:00+00:00', + 'end': '2025-09-30T23:30:00+00:00', + 'price': 509.05, + 'start': '2025-09-30T23:15:00+00:00', }), dict({ - 'end': '2024-11-05T06:00:00+00:00', - 'price': 996.28, - 'start': '2024-11-05T05:00:00+00:00', + 'end': '2025-09-30T23:45:00+00:00', + 'price': 491.03, + 'start': '2025-09-30T23:30:00+00:00', }), dict({ - 'end': '2024-11-05T07:00:00+00:00', - 'price': 1406.14, - 'start': '2024-11-05T06:00:00+00:00', + 'end': '2025-10-01T00:00:00+00:00', + 'price': 442.07, + 'start': '2025-09-30T23:45:00+00:00', }), dict({ - 'end': '2024-11-05T08:00:00+00:00', - 'price': 1346.54, - 'start': '2024-11-05T07:00:00+00:00', + 'end': '2025-10-01T00:15:00+00:00', + 'price': 504.08, + 'start': '2025-10-01T00:00:00+00:00', }), dict({ - 'end': '2024-11-05T09:00:00+00:00', - 'price': 1150.28, - 'start': '2024-11-05T08:00:00+00:00', + 'end': '2025-10-01T00:30:00+00:00', + 'price': 504.85, + 'start': '2025-10-01T00:15:00+00:00', }), dict({ - 'end': '2024-11-05T10:00:00+00:00', - 'price': 1031.32, - 'start': '2024-11-05T09:00:00+00:00', + 'end': '2025-10-01T00:45:00+00:00', + 'price': 504.3, + 'start': '2025-10-01T00:30:00+00:00', }), dict({ - 'end': '2024-11-05T11:00:00+00:00', - 'price': 927.37, - 'start': '2024-11-05T10:00:00+00:00', + 'end': '2025-10-01T01:00:00+00:00', + 'price': 506.29, + 'start': '2025-10-01T00:45:00+00:00', }), dict({ - 'end': '2024-11-05T12:00:00+00:00', - 'price': 925.05, - 'start': '2024-11-05T11:00:00+00:00', + 'end': '2025-10-01T01:15:00+00:00', + 'price': 442.07, + 'start': '2025-10-01T01:00:00+00:00', }), dict({ - 'end': '2024-11-05T13:00:00+00:00', - 'price': 949.49, - 'start': '2024-11-05T12:00:00+00:00', + 'end': '2025-10-01T01:30:00+00:00', + 'price': 441.96, + 'start': '2025-10-01T01:15:00+00:00', }), dict({ - 'end': '2024-11-05T14:00:00+00:00', - 'price': 1042.03, - 'start': '2024-11-05T13:00:00+00:00', + 'end': '2025-10-01T01:45:00+00:00', + 'price': 442.07, + 'start': '2025-10-01T01:30:00+00:00', }), dict({ - 'end': '2024-11-05T15:00:00+00:00', - 'price': 1258.89, - 'start': '2024-11-05T14:00:00+00:00', + 'end': '2025-10-01T02:00:00+00:00', + 'price': 442.07, + 'start': '2025-10-01T01:45:00+00:00', }), dict({ - 'end': '2024-11-05T16:00:00+00:00', - 'price': 1816.45, - 'start': '2024-11-05T15:00:00+00:00', + 'end': '2025-10-01T02:15:00+00:00', + 'price': 441.96, + 'start': '2025-10-01T02:00:00+00:00', }), dict({ - 'end': '2024-11-05T17:00:00+00:00', - 'price': 2512.65, - 'start': '2024-11-05T16:00:00+00:00', + 'end': '2025-10-01T02:30:00+00:00', + 'price': 483.3, + 'start': '2025-10-01T02:15:00+00:00', }), dict({ - 'end': '2024-11-05T18:00:00+00:00', - 'price': 1819.83, - 'start': '2024-11-05T17:00:00+00:00', + 'end': '2025-10-01T02:45:00+00:00', + 'price': 484.29, + 'start': '2025-10-01T02:30:00+00:00', }), dict({ - 'end': '2024-11-05T19:00:00+00:00', - 'price': 1011.77, - 'start': '2024-11-05T18:00:00+00:00', + 'end': '2025-10-01T03:00:00+00:00', + 'price': 574.7, + 'start': '2025-10-01T02:45:00+00:00', }), dict({ - 'end': '2024-11-05T20:00:00+00:00', - 'price': 835.53, - 'start': '2024-11-05T19:00:00+00:00', + 'end': '2025-10-01T03:15:00+00:00', + 'price': 543.31, + 'start': '2025-10-01T03:00:00+00:00', }), dict({ - 'end': '2024-11-05T21:00:00+00:00', - 'price': 796.19, - 'start': '2024-11-05T20:00:00+00:00', + 'end': '2025-10-01T03:30:00+00:00', + 'price': 578.01, + 'start': '2025-10-01T03:15:00+00:00', }), dict({ - 'end': '2024-11-05T22:00:00+00:00', - 'price': 522.3, - 'start': '2024-11-05T21:00:00+00:00', + 'end': '2025-10-01T03:45:00+00:00', + 'price': 774.96, + 'start': '2025-10-01T03:30:00+00:00', }), dict({ - 'end': '2024-11-05T23:00:00+00:00', - 'price': 289.14, - 'start': '2024-11-05T22:00:00+00:00', + 'end': '2025-10-01T04:00:00+00:00', + 'price': 787.0, + 'start': '2025-10-01T03:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T04:15:00+00:00', + 'price': 902.38, + 'start': '2025-10-01T04:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T04:30:00+00:00', + 'price': 1079.32, + 'start': '2025-10-01T04:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T04:45:00+00:00', + 'price': 1222.67, + 'start': '2025-10-01T04:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T05:00:00+00:00', + 'price': 1394.63, + 'start': '2025-10-01T04:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T05:15:00+00:00', + 'price': 1529.36, + 'start': '2025-10-01T05:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T05:30:00+00:00', + 'price': 1724.53, + 'start': '2025-10-01T05:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T05:45:00+00:00', + 'price': 1809.96, + 'start': '2025-10-01T05:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T06:00:00+00:00', + 'price': 1713.04, + 'start': '2025-10-01T05:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T06:15:00+00:00', + 'price': 1925.9, + 'start': '2025-10-01T06:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T06:30:00+00:00', + 'price': 1440.06, + 'start': '2025-10-01T06:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T06:45:00+00:00', + 'price': 1183.32, + 'start': '2025-10-01T06:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T07:00:00+00:00', + 'price': 962.95, + 'start': '2025-10-01T06:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T07:15:00+00:00', + 'price': 1402.04, + 'start': '2025-10-01T07:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T07:30:00+00:00', + 'price': 1060.65, + 'start': '2025-10-01T07:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T07:45:00+00:00', + 'price': 949.13, + 'start': '2025-10-01T07:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T08:00:00+00:00', + 'price': 841.82, + 'start': '2025-10-01T07:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T08:15:00+00:00', + 'price': 1037.44, + 'start': '2025-10-01T08:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T08:30:00+00:00', + 'price': 950.13, + 'start': '2025-10-01T08:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T08:45:00+00:00', + 'price': 826.13, + 'start': '2025-10-01T08:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T09:00:00+00:00', + 'price': 684.55, + 'start': '2025-10-01T08:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T09:15:00+00:00', + 'price': 861.6, + 'start': '2025-10-01T09:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T09:30:00+00:00', + 'price': 722.79, + 'start': '2025-10-01T09:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T09:45:00+00:00', + 'price': 640.57, + 'start': '2025-10-01T09:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T10:00:00+00:00', + 'price': 607.74, + 'start': '2025-10-01T09:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T10:15:00+00:00', + 'price': 674.05, + 'start': '2025-10-01T10:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T10:30:00+00:00', + 'price': 638.58, + 'start': '2025-10-01T10:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T10:45:00+00:00', + 'price': 638.47, + 'start': '2025-10-01T10:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T11:00:00+00:00', + 'price': 634.82, + 'start': '2025-10-01T10:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T11:15:00+00:00', + 'price': 637.36, + 'start': '2025-10-01T11:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T11:30:00+00:00', + 'price': 660.68, + 'start': '2025-10-01T11:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T11:45:00+00:00', + 'price': 679.14, + 'start': '2025-10-01T11:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T12:00:00+00:00', + 'price': 694.61, + 'start': '2025-10-01T11:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T12:15:00+00:00', + 'price': 622.33, + 'start': '2025-10-01T12:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T12:30:00+00:00', + 'price': 685.44, + 'start': '2025-10-01T12:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T12:45:00+00:00', + 'price': 732.85, + 'start': '2025-10-01T12:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T13:00:00+00:00', + 'price': 801.92, + 'start': '2025-10-01T12:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T13:15:00+00:00', + 'price': 629.4, + 'start': '2025-10-01T13:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T13:30:00+00:00', + 'price': 729.53, + 'start': '2025-10-01T13:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T13:45:00+00:00', + 'price': 884.81, + 'start': '2025-10-01T13:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T14:00:00+00:00', + 'price': 984.94, + 'start': '2025-10-01T13:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T14:15:00+00:00', + 'price': 615.26, + 'start': '2025-10-01T14:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T14:30:00+00:00', + 'price': 902.94, + 'start': '2025-10-01T14:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T14:45:00+00:00', + 'price': 1043.85, + 'start': '2025-10-01T14:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T15:00:00+00:00', + 'price': 1075.12, + 'start': '2025-10-01T14:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T15:15:00+00:00', + 'price': 980.52, + 'start': '2025-10-01T15:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T15:30:00+00:00', + 'price': 1162.66, + 'start': '2025-10-01T15:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T15:45:00+00:00', + 'price': 1453.87, + 'start': '2025-10-01T15:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T16:00:00+00:00', + 'price': 1955.96, + 'start': '2025-10-01T15:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T16:15:00+00:00', + 'price': 1423.48, + 'start': '2025-10-01T16:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T16:30:00+00:00', + 'price': 1900.04, + 'start': '2025-10-01T16:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T16:45:00+00:00', + 'price': 2611.11, + 'start': '2025-10-01T16:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T17:00:00+00:00', + 'price': 3467.41, + 'start': '2025-10-01T16:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T17:15:00+00:00', + 'price': 3828.03, + 'start': '2025-10-01T17:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T17:30:00+00:00', + 'price': 3429.83, + 'start': '2025-10-01T17:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T17:45:00+00:00', + 'price': 2934.38, + 'start': '2025-10-01T17:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T18:00:00+00:00', + 'price': 2308.07, + 'start': '2025-10-01T17:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T18:15:00+00:00', + 'price': 1997.96, + 'start': '2025-10-01T18:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T18:30:00+00:00', + 'price': 1424.03, + 'start': '2025-10-01T18:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T18:45:00+00:00', + 'price': 1216.81, + 'start': '2025-10-01T18:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T19:00:00+00:00', + 'price': 1070.15, + 'start': '2025-10-01T18:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T19:15:00+00:00', + 'price': 1218.14, + 'start': '2025-10-01T19:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T19:30:00+00:00', + 'price': 1135.8, + 'start': '2025-10-01T19:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T19:45:00+00:00', + 'price': 959.96, + 'start': '2025-10-01T19:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T20:00:00+00:00', + 'price': 913.66, + 'start': '2025-10-01T19:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T20:15:00+00:00', + 'price': 1001.63, + 'start': '2025-10-01T20:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T20:30:00+00:00', + 'price': 933.0, + 'start': '2025-10-01T20:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T20:45:00+00:00', + 'price': 874.53, + 'start': '2025-10-01T20:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T21:00:00+00:00', + 'price': 821.71, + 'start': '2025-10-01T20:45:00+00:00', + }), + dict({ + 'end': '2025-10-01T21:15:00+00:00', + 'price': 860.5, + 'start': '2025-10-01T21:00:00+00:00', + }), + dict({ + 'end': '2025-10-01T21:30:00+00:00', + 'price': 840.16, + 'start': '2025-10-01T21:15:00+00:00', + }), + dict({ + 'end': '2025-10-01T21:45:00+00:00', + 'price': 820.05, + 'start': '2025-10-01T21:30:00+00:00', + }), + dict({ + 'end': '2025-10-01T22:00:00+00:00', + 'price': 785.68, + 'start': '2025-10-01T21:45:00+00:00', }), ]), }) @@ -135,484 +495,484 @@ dict({ 'SE3': list([ dict({ - 'end': '2025-07-05T22:15:00+00:00', - 'price': 43.57, - 'start': '2025-07-05T22:00:00+00:00', + 'end': '2025-09-30T22:15:00+00:00', + 'price': 556.68, + 'start': '2025-09-30T22:00:00+00:00', }), dict({ - 'end': '2025-07-05T22:30:00+00:00', - 'price': 43.57, - 'start': '2025-07-05T22:15:00+00:00', + 'end': '2025-09-30T22:30:00+00:00', + 'price': 519.88, + 'start': '2025-09-30T22:15:00+00:00', }), dict({ - 'end': '2025-07-05T22:45:00+00:00', - 'price': 43.57, - 'start': '2025-07-05T22:30:00+00:00', + 'end': '2025-09-30T22:45:00+00:00', + 'price': 508.28, + 'start': '2025-09-30T22:30:00+00:00', }), dict({ - 'end': '2025-07-05T23:00:00+00:00', - 'price': 43.57, - 'start': '2025-07-05T22:45:00+00:00', + 'end': '2025-09-30T23:00:00+00:00', + 'price': 509.93, + 'start': '2025-09-30T22:45:00+00:00', }), dict({ - 'end': '2025-07-05T23:15:00+00:00', - 'price': 36.47, - 'start': '2025-07-05T23:00:00+00:00', + 'end': '2025-09-30T23:15:00+00:00', + 'price': 501.64, + 'start': '2025-09-30T23:00:00+00:00', }), dict({ - 'end': '2025-07-05T23:30:00+00:00', - 'price': 36.47, - 'start': '2025-07-05T23:15:00+00:00', + 'end': '2025-09-30T23:30:00+00:00', + 'price': 509.05, + 'start': '2025-09-30T23:15:00+00:00', }), dict({ - 'end': '2025-07-05T23:45:00+00:00', - 'price': 36.47, - 'start': '2025-07-05T23:30:00+00:00', + 'end': '2025-09-30T23:45:00+00:00', + 'price': 491.03, + 'start': '2025-09-30T23:30:00+00:00', }), dict({ - 'end': '2025-07-06T00:00:00+00:00', - 'price': 36.47, - 'start': '2025-07-05T23:45:00+00:00', + 'end': '2025-10-01T00:00:00+00:00', + 'price': 442.07, + 'start': '2025-09-30T23:45:00+00:00', }), dict({ - 'end': '2025-07-06T00:15:00+00:00', - 'price': 35.57, - 'start': '2025-07-06T00:00:00+00:00', + 'end': '2025-10-01T00:15:00+00:00', + 'price': 504.08, + 'start': '2025-10-01T00:00:00+00:00', }), dict({ - 'end': '2025-07-06T00:30:00+00:00', - 'price': 35.57, - 'start': '2025-07-06T00:15:00+00:00', + 'end': '2025-10-01T00:30:00+00:00', + 'price': 504.85, + 'start': '2025-10-01T00:15:00+00:00', }), dict({ - 'end': '2025-07-06T00:45:00+00:00', - 'price': 35.57, - 'start': '2025-07-06T00:30:00+00:00', + 'end': '2025-10-01T00:45:00+00:00', + 'price': 504.3, + 'start': '2025-10-01T00:30:00+00:00', }), dict({ - 'end': '2025-07-06T01:00:00+00:00', - 'price': 35.57, - 'start': '2025-07-06T00:45:00+00:00', + 'end': '2025-10-01T01:00:00+00:00', + 'price': 506.29, + 'start': '2025-10-01T00:45:00+00:00', }), dict({ - 'end': '2025-07-06T01:15:00+00:00', - 'price': 30.73, - 'start': '2025-07-06T01:00:00+00:00', + 'end': '2025-10-01T01:15:00+00:00', + 'price': 442.07, + 'start': '2025-10-01T01:00:00+00:00', }), dict({ - 'end': '2025-07-06T01:30:00+00:00', - 'price': 30.73, - 'start': '2025-07-06T01:15:00+00:00', + 'end': '2025-10-01T01:30:00+00:00', + 'price': 441.96, + 'start': '2025-10-01T01:15:00+00:00', }), dict({ - 'end': '2025-07-06T01:45:00+00:00', - 'price': 30.73, - 'start': '2025-07-06T01:30:00+00:00', + 'end': '2025-10-01T01:45:00+00:00', + 'price': 442.07, + 'start': '2025-10-01T01:30:00+00:00', }), dict({ - 'end': '2025-07-06T02:00:00+00:00', - 'price': 30.73, - 'start': '2025-07-06T01:45:00+00:00', + 'end': '2025-10-01T02:00:00+00:00', + 'price': 442.07, + 'start': '2025-10-01T01:45:00+00:00', }), dict({ - 'end': '2025-07-06T02:15:00+00:00', - 'price': 32.42, - 'start': '2025-07-06T02:00:00+00:00', + 'end': '2025-10-01T02:15:00+00:00', + 'price': 441.96, + 'start': '2025-10-01T02:00:00+00:00', }), dict({ - 'end': '2025-07-06T02:30:00+00:00', - 'price': 32.42, - 'start': '2025-07-06T02:15:00+00:00', + 'end': '2025-10-01T02:30:00+00:00', + 'price': 483.3, + 'start': '2025-10-01T02:15:00+00:00', }), dict({ - 'end': '2025-07-06T02:45:00+00:00', - 'price': 32.42, - 'start': '2025-07-06T02:30:00+00:00', + 'end': '2025-10-01T02:45:00+00:00', + 'price': 484.29, + 'start': '2025-10-01T02:30:00+00:00', }), dict({ - 'end': '2025-07-06T03:00:00+00:00', - 'price': 32.42, - 'start': '2025-07-06T02:45:00+00:00', + 'end': '2025-10-01T03:00:00+00:00', + 'price': 574.7, + 'start': '2025-10-01T02:45:00+00:00', }), dict({ - 'end': '2025-07-06T03:15:00+00:00', - 'price': 38.73, - 'start': '2025-07-06T03:00:00+00:00', + 'end': '2025-10-01T03:15:00+00:00', + 'price': 543.31, + 'start': '2025-10-01T03:00:00+00:00', }), dict({ - 'end': '2025-07-06T03:30:00+00:00', - 'price': 38.73, - 'start': '2025-07-06T03:15:00+00:00', + 'end': '2025-10-01T03:30:00+00:00', + 'price': 578.01, + 'start': '2025-10-01T03:15:00+00:00', }), dict({ - 'end': '2025-07-06T03:45:00+00:00', - 'price': 38.73, - 'start': '2025-07-06T03:30:00+00:00', + 'end': '2025-10-01T03:45:00+00:00', + 'price': 774.96, + 'start': '2025-10-01T03:30:00+00:00', }), dict({ - 'end': '2025-07-06T04:00:00+00:00', - 'price': 38.73, - 'start': '2025-07-06T03:45:00+00:00', + 'end': '2025-10-01T04:00:00+00:00', + 'price': 787.0, + 'start': '2025-10-01T03:45:00+00:00', }), dict({ - 'end': '2025-07-06T04:15:00+00:00', - 'price': 42.78, - 'start': '2025-07-06T04:00:00+00:00', + 'end': '2025-10-01T04:15:00+00:00', + 'price': 902.38, + 'start': '2025-10-01T04:00:00+00:00', }), dict({ - 'end': '2025-07-06T04:30:00+00:00', - 'price': 42.78, - 'start': '2025-07-06T04:15:00+00:00', + 'end': '2025-10-01T04:30:00+00:00', + 'price': 1079.32, + 'start': '2025-10-01T04:15:00+00:00', }), dict({ - 'end': '2025-07-06T04:45:00+00:00', - 'price': 42.78, - 'start': '2025-07-06T04:30:00+00:00', + 'end': '2025-10-01T04:45:00+00:00', + 'price': 1222.67, + 'start': '2025-10-01T04:30:00+00:00', }), dict({ - 'end': '2025-07-06T05:00:00+00:00', - 'price': 42.78, - 'start': '2025-07-06T04:45:00+00:00', + 'end': '2025-10-01T05:00:00+00:00', + 'price': 1394.63, + 'start': '2025-10-01T04:45:00+00:00', }), dict({ - 'end': '2025-07-06T05:15:00+00:00', - 'price': 54.71, - 'start': '2025-07-06T05:00:00+00:00', + 'end': '2025-10-01T05:15:00+00:00', + 'price': 1529.36, + 'start': '2025-10-01T05:00:00+00:00', }), dict({ - 'end': '2025-07-06T05:30:00+00:00', - 'price': 54.71, - 'start': '2025-07-06T05:15:00+00:00', + 'end': '2025-10-01T05:30:00+00:00', + 'price': 1724.53, + 'start': '2025-10-01T05:15:00+00:00', }), dict({ - 'end': '2025-07-06T05:45:00+00:00', - 'price': 54.71, - 'start': '2025-07-06T05:30:00+00:00', + 'end': '2025-10-01T05:45:00+00:00', + 'price': 1809.96, + 'start': '2025-10-01T05:30:00+00:00', }), dict({ - 'end': '2025-07-06T06:00:00+00:00', - 'price': 54.71, - 'start': '2025-07-06T05:45:00+00:00', + 'end': '2025-10-01T06:00:00+00:00', + 'price': 1713.04, + 'start': '2025-10-01T05:45:00+00:00', }), dict({ - 'end': '2025-07-06T06:15:00+00:00', - 'price': 83.87, - 'start': '2025-07-06T06:00:00+00:00', + 'end': '2025-10-01T06:15:00+00:00', + 'price': 1925.9, + 'start': '2025-10-01T06:00:00+00:00', }), dict({ - 'end': '2025-07-06T06:30:00+00:00', - 'price': 83.87, - 'start': '2025-07-06T06:15:00+00:00', + 'end': '2025-10-01T06:30:00+00:00', + 'price': 1440.06, + 'start': '2025-10-01T06:15:00+00:00', }), dict({ - 'end': '2025-07-06T06:45:00+00:00', - 'price': 83.87, - 'start': '2025-07-06T06:30:00+00:00', + 'end': '2025-10-01T06:45:00+00:00', + 'price': 1183.32, + 'start': '2025-10-01T06:30:00+00:00', }), dict({ - 'end': '2025-07-06T07:00:00+00:00', - 'price': 83.87, - 'start': '2025-07-06T06:45:00+00:00', + 'end': '2025-10-01T07:00:00+00:00', + 'price': 962.95, + 'start': '2025-10-01T06:45:00+00:00', }), dict({ - 'end': '2025-07-06T07:15:00+00:00', - 'price': 78.8, - 'start': '2025-07-06T07:00:00+00:00', + 'end': '2025-10-01T07:15:00+00:00', + 'price': 1402.04, + 'start': '2025-10-01T07:00:00+00:00', }), dict({ - 'end': '2025-07-06T07:30:00+00:00', - 'price': 78.8, - 'start': '2025-07-06T07:15:00+00:00', + 'end': '2025-10-01T07:30:00+00:00', + 'price': 1060.65, + 'start': '2025-10-01T07:15:00+00:00', }), dict({ - 'end': '2025-07-06T07:45:00+00:00', - 'price': 78.8, - 'start': '2025-07-06T07:30:00+00:00', + 'end': '2025-10-01T07:45:00+00:00', + 'price': 949.13, + 'start': '2025-10-01T07:30:00+00:00', }), dict({ - 'end': '2025-07-06T08:00:00+00:00', - 'price': 78.8, - 'start': '2025-07-06T07:45:00+00:00', + 'end': '2025-10-01T08:00:00+00:00', + 'price': 841.82, + 'start': '2025-10-01T07:45:00+00:00', }), dict({ - 'end': '2025-07-06T08:15:00+00:00', - 'price': 92.09, - 'start': '2025-07-06T08:00:00+00:00', + 'end': '2025-10-01T08:15:00+00:00', + 'price': 1037.44, + 'start': '2025-10-01T08:00:00+00:00', }), dict({ - 'end': '2025-07-06T08:30:00+00:00', - 'price': 92.09, - 'start': '2025-07-06T08:15:00+00:00', + 'end': '2025-10-01T08:30:00+00:00', + 'price': 950.13, + 'start': '2025-10-01T08:15:00+00:00', }), dict({ - 'end': '2025-07-06T08:45:00+00:00', - 'price': 92.09, - 'start': '2025-07-06T08:30:00+00:00', + 'end': '2025-10-01T08:45:00+00:00', + 'price': 826.13, + 'start': '2025-10-01T08:30:00+00:00', }), dict({ - 'end': '2025-07-06T09:00:00+00:00', - 'price': 92.09, - 'start': '2025-07-06T08:45:00+00:00', + 'end': '2025-10-01T09:00:00+00:00', + 'price': 684.55, + 'start': '2025-10-01T08:45:00+00:00', }), dict({ - 'end': '2025-07-06T09:15:00+00:00', - 'price': 104.92, - 'start': '2025-07-06T09:00:00+00:00', + 'end': '2025-10-01T09:15:00+00:00', + 'price': 861.6, + 'start': '2025-10-01T09:00:00+00:00', }), dict({ - 'end': '2025-07-06T09:30:00+00:00', - 'price': 104.92, - 'start': '2025-07-06T09:15:00+00:00', + 'end': '2025-10-01T09:30:00+00:00', + 'price': 722.79, + 'start': '2025-10-01T09:15:00+00:00', }), dict({ - 'end': '2025-07-06T09:45:00+00:00', - 'price': 104.92, - 'start': '2025-07-06T09:30:00+00:00', + 'end': '2025-10-01T09:45:00+00:00', + 'price': 640.57, + 'start': '2025-10-01T09:30:00+00:00', }), dict({ - 'end': '2025-07-06T10:00:00+00:00', - 'price': 104.92, - 'start': '2025-07-06T09:45:00+00:00', + 'end': '2025-10-01T10:00:00+00:00', + 'price': 607.74, + 'start': '2025-10-01T09:45:00+00:00', }), dict({ - 'end': '2025-07-06T10:15:00+00:00', - 'price': 72.5, - 'start': '2025-07-06T10:00:00+00:00', + 'end': '2025-10-01T10:15:00+00:00', + 'price': 674.05, + 'start': '2025-10-01T10:00:00+00:00', }), dict({ - 'end': '2025-07-06T10:30:00+00:00', - 'price': 72.5, - 'start': '2025-07-06T10:15:00+00:00', + 'end': '2025-10-01T10:30:00+00:00', + 'price': 638.58, + 'start': '2025-10-01T10:15:00+00:00', }), dict({ - 'end': '2025-07-06T10:45:00+00:00', - 'price': 72.5, - 'start': '2025-07-06T10:30:00+00:00', + 'end': '2025-10-01T10:45:00+00:00', + 'price': 638.47, + 'start': '2025-10-01T10:30:00+00:00', }), dict({ - 'end': '2025-07-06T11:00:00+00:00', - 'price': 72.5, - 'start': '2025-07-06T10:45:00+00:00', + 'end': '2025-10-01T11:00:00+00:00', + 'price': 634.82, + 'start': '2025-10-01T10:45:00+00:00', }), dict({ - 'end': '2025-07-06T11:15:00+00:00', - 'price': 63.49, - 'start': '2025-07-06T11:00:00+00:00', + 'end': '2025-10-01T11:15:00+00:00', + 'price': 637.36, + 'start': '2025-10-01T11:00:00+00:00', }), dict({ - 'end': '2025-07-06T11:30:00+00:00', - 'price': 63.49, - 'start': '2025-07-06T11:15:00+00:00', + 'end': '2025-10-01T11:30:00+00:00', + 'price': 660.68, + 'start': '2025-10-01T11:15:00+00:00', }), dict({ - 'end': '2025-07-06T11:45:00+00:00', - 'price': 63.49, - 'start': '2025-07-06T11:30:00+00:00', + 'end': '2025-10-01T11:45:00+00:00', + 'price': 679.14, + 'start': '2025-10-01T11:30:00+00:00', }), dict({ - 'end': '2025-07-06T12:00:00+00:00', - 'price': 63.49, - 'start': '2025-07-06T11:45:00+00:00', + 'end': '2025-10-01T12:00:00+00:00', + 'price': 694.61, + 'start': '2025-10-01T11:45:00+00:00', }), dict({ - 'end': '2025-07-06T12:15:00+00:00', - 'price': 91.64, - 'start': '2025-07-06T12:00:00+00:00', + 'end': '2025-10-01T12:15:00+00:00', + 'price': 622.33, + 'start': '2025-10-01T12:00:00+00:00', }), dict({ - 'end': '2025-07-06T12:30:00+00:00', - 'price': 91.64, - 'start': '2025-07-06T12:15:00+00:00', + 'end': '2025-10-01T12:30:00+00:00', + 'price': 685.44, + 'start': '2025-10-01T12:15:00+00:00', }), dict({ - 'end': '2025-07-06T12:45:00+00:00', - 'price': 91.64, - 'start': '2025-07-06T12:30:00+00:00', + 'end': '2025-10-01T12:45:00+00:00', + 'price': 732.85, + 'start': '2025-10-01T12:30:00+00:00', }), dict({ - 'end': '2025-07-06T13:00:00+00:00', - 'price': 91.64, - 'start': '2025-07-06T12:45:00+00:00', + 'end': '2025-10-01T13:00:00+00:00', + 'price': 801.92, + 'start': '2025-10-01T12:45:00+00:00', }), dict({ - 'end': '2025-07-06T13:15:00+00:00', - 'price': 111.79, - 'start': '2025-07-06T13:00:00+00:00', + 'end': '2025-10-01T13:15:00+00:00', + 'price': 629.4, + 'start': '2025-10-01T13:00:00+00:00', }), dict({ - 'end': '2025-07-06T13:30:00+00:00', - 'price': 111.79, - 'start': '2025-07-06T13:15:00+00:00', + 'end': '2025-10-01T13:30:00+00:00', + 'price': 729.53, + 'start': '2025-10-01T13:15:00+00:00', }), dict({ - 'end': '2025-07-06T13:45:00+00:00', - 'price': 111.79, - 'start': '2025-07-06T13:30:00+00:00', + 'end': '2025-10-01T13:45:00+00:00', + 'price': 884.81, + 'start': '2025-10-01T13:30:00+00:00', }), dict({ - 'end': '2025-07-06T14:00:00+00:00', - 'price': 111.79, - 'start': '2025-07-06T13:45:00+00:00', + 'end': '2025-10-01T14:00:00+00:00', + 'price': 984.94, + 'start': '2025-10-01T13:45:00+00:00', }), dict({ - 'end': '2025-07-06T14:15:00+00:00', - 'price': 234.04, - 'start': '2025-07-06T14:00:00+00:00', + 'end': '2025-10-01T14:15:00+00:00', + 'price': 615.26, + 'start': '2025-10-01T14:00:00+00:00', }), dict({ - 'end': '2025-07-06T14:30:00+00:00', - 'price': 234.04, - 'start': '2025-07-06T14:15:00+00:00', + 'end': '2025-10-01T14:30:00+00:00', + 'price': 902.94, + 'start': '2025-10-01T14:15:00+00:00', }), dict({ - 'end': '2025-07-06T14:45:00+00:00', - 'price': 234.04, - 'start': '2025-07-06T14:30:00+00:00', + 'end': '2025-10-01T14:45:00+00:00', + 'price': 1043.85, + 'start': '2025-10-01T14:30:00+00:00', }), dict({ - 'end': '2025-07-06T15:00:00+00:00', - 'price': 234.04, - 'start': '2025-07-06T14:45:00+00:00', + 'end': '2025-10-01T15:00:00+00:00', + 'price': 1075.12, + 'start': '2025-10-01T14:45:00+00:00', }), dict({ - 'end': '2025-07-06T15:15:00+00:00', - 'price': 435.33, - 'start': '2025-07-06T15:00:00+00:00', + 'end': '2025-10-01T15:15:00+00:00', + 'price': 980.52, + 'start': '2025-10-01T15:00:00+00:00', }), dict({ - 'end': '2025-07-06T15:30:00+00:00', - 'price': 435.33, - 'start': '2025-07-06T15:15:00+00:00', + 'end': '2025-10-01T15:30:00+00:00', + 'price': 1162.66, + 'start': '2025-10-01T15:15:00+00:00', }), dict({ - 'end': '2025-07-06T15:45:00+00:00', - 'price': 435.33, - 'start': '2025-07-06T15:30:00+00:00', + 'end': '2025-10-01T15:45:00+00:00', + 'price': 1453.87, + 'start': '2025-10-01T15:30:00+00:00', }), dict({ - 'end': '2025-07-06T16:00:00+00:00', - 'price': 435.33, - 'start': '2025-07-06T15:45:00+00:00', + 'end': '2025-10-01T16:00:00+00:00', + 'price': 1955.96, + 'start': '2025-10-01T15:45:00+00:00', }), dict({ - 'end': '2025-07-06T16:15:00+00:00', - 'price': 431.84, - 'start': '2025-07-06T16:00:00+00:00', + 'end': '2025-10-01T16:15:00+00:00', + 'price': 1423.48, + 'start': '2025-10-01T16:00:00+00:00', }), dict({ - 'end': '2025-07-06T16:30:00+00:00', - 'price': 431.84, - 'start': '2025-07-06T16:15:00+00:00', + 'end': '2025-10-01T16:30:00+00:00', + 'price': 1900.04, + 'start': '2025-10-01T16:15:00+00:00', }), dict({ - 'end': '2025-07-06T16:45:00+00:00', - 'price': 431.84, - 'start': '2025-07-06T16:30:00+00:00', + 'end': '2025-10-01T16:45:00+00:00', + 'price': 2611.11, + 'start': '2025-10-01T16:30:00+00:00', }), dict({ - 'end': '2025-07-06T17:00:00+00:00', - 'price': 431.84, - 'start': '2025-07-06T16:45:00+00:00', + 'end': '2025-10-01T17:00:00+00:00', + 'price': 3467.41, + 'start': '2025-10-01T16:45:00+00:00', }), dict({ - 'end': '2025-07-06T17:15:00+00:00', - 'price': 423.73, - 'start': '2025-07-06T17:00:00+00:00', + 'end': '2025-10-01T17:15:00+00:00', + 'price': 3828.03, + 'start': '2025-10-01T17:00:00+00:00', }), dict({ - 'end': '2025-07-06T17:30:00+00:00', - 'price': 423.73, - 'start': '2025-07-06T17:15:00+00:00', + 'end': '2025-10-01T17:30:00+00:00', + 'price': 3429.83, + 'start': '2025-10-01T17:15:00+00:00', }), dict({ - 'end': '2025-07-06T17:45:00+00:00', - 'price': 423.73, - 'start': '2025-07-06T17:30:00+00:00', + 'end': '2025-10-01T17:45:00+00:00', + 'price': 2934.38, + 'start': '2025-10-01T17:30:00+00:00', }), dict({ - 'end': '2025-07-06T18:00:00+00:00', - 'price': 423.73, - 'start': '2025-07-06T17:45:00+00:00', + 'end': '2025-10-01T18:00:00+00:00', + 'price': 2308.07, + 'start': '2025-10-01T17:45:00+00:00', }), dict({ - 'end': '2025-07-06T18:15:00+00:00', - 'price': 437.92, - 'start': '2025-07-06T18:00:00+00:00', + 'end': '2025-10-01T18:15:00+00:00', + 'price': 1997.96, + 'start': '2025-10-01T18:00:00+00:00', }), dict({ - 'end': '2025-07-06T18:30:00+00:00', - 'price': 437.92, - 'start': '2025-07-06T18:15:00+00:00', + 'end': '2025-10-01T18:30:00+00:00', + 'price': 1424.03, + 'start': '2025-10-01T18:15:00+00:00', }), dict({ - 'end': '2025-07-06T18:45:00+00:00', - 'price': 437.92, - 'start': '2025-07-06T18:30:00+00:00', + 'end': '2025-10-01T18:45:00+00:00', + 'price': 1216.81, + 'start': '2025-10-01T18:30:00+00:00', }), dict({ - 'end': '2025-07-06T19:00:00+00:00', - 'price': 437.92, - 'start': '2025-07-06T18:45:00+00:00', + 'end': '2025-10-01T19:00:00+00:00', + 'price': 1070.15, + 'start': '2025-10-01T18:45:00+00:00', }), dict({ - 'end': '2025-07-06T19:15:00+00:00', - 'price': 416.42, - 'start': '2025-07-06T19:00:00+00:00', + 'end': '2025-10-01T19:15:00+00:00', + 'price': 1218.14, + 'start': '2025-10-01T19:00:00+00:00', }), dict({ - 'end': '2025-07-06T19:30:00+00:00', - 'price': 416.42, - 'start': '2025-07-06T19:15:00+00:00', + 'end': '2025-10-01T19:30:00+00:00', + 'price': 1135.8, + 'start': '2025-10-01T19:15:00+00:00', }), dict({ - 'end': '2025-07-06T19:45:00+00:00', - 'price': 416.42, - 'start': '2025-07-06T19:30:00+00:00', + 'end': '2025-10-01T19:45:00+00:00', + 'price': 959.96, + 'start': '2025-10-01T19:30:00+00:00', }), dict({ - 'end': '2025-07-06T20:00:00+00:00', - 'price': 416.42, - 'start': '2025-07-06T19:45:00+00:00', + 'end': '2025-10-01T20:00:00+00:00', + 'price': 913.66, + 'start': '2025-10-01T19:45:00+00:00', }), dict({ - 'end': '2025-07-06T20:15:00+00:00', - 'price': 414.39, - 'start': '2025-07-06T20:00:00+00:00', + 'end': '2025-10-01T20:15:00+00:00', + 'price': 1001.63, + 'start': '2025-10-01T20:00:00+00:00', }), dict({ - 'end': '2025-07-06T20:30:00+00:00', - 'price': 414.39, - 'start': '2025-07-06T20:15:00+00:00', + 'end': '2025-10-01T20:30:00+00:00', + 'price': 933.0, + 'start': '2025-10-01T20:15:00+00:00', }), dict({ - 'end': '2025-07-06T20:45:00+00:00', - 'price': 414.39, - 'start': '2025-07-06T20:30:00+00:00', + 'end': '2025-10-01T20:45:00+00:00', + 'price': 874.53, + 'start': '2025-10-01T20:30:00+00:00', }), dict({ - 'end': '2025-07-06T21:00:00+00:00', - 'price': 414.39, - 'start': '2025-07-06T20:45:00+00:00', + 'end': '2025-10-01T21:00:00+00:00', + 'price': 821.71, + 'start': '2025-10-01T20:45:00+00:00', }), dict({ - 'end': '2025-07-06T21:15:00+00:00', - 'price': 396.38, - 'start': '2025-07-06T21:00:00+00:00', + 'end': '2025-10-01T21:15:00+00:00', + 'price': 860.5, + 'start': '2025-10-01T21:00:00+00:00', }), dict({ - 'end': '2025-07-06T21:30:00+00:00', - 'price': 396.38, - 'start': '2025-07-06T21:15:00+00:00', + 'end': '2025-10-01T21:30:00+00:00', + 'price': 840.16, + 'start': '2025-10-01T21:15:00+00:00', }), dict({ - 'end': '2025-07-06T21:45:00+00:00', - 'price': 396.38, - 'start': '2025-07-06T21:30:00+00:00', + 'end': '2025-10-01T21:45:00+00:00', + 'price': 820.05, + 'start': '2025-10-01T21:30:00+00:00', }), dict({ - 'end': '2025-07-06T22:00:00+00:00', - 'price': 396.38, - 'start': '2025-07-06T21:45:00+00:00', + 'end': '2025-10-01T22:00:00+00:00', + 'price': 785.68, + 'start': '2025-10-01T21:45:00+00:00', }), ]), }) @@ -621,124 +981,124 @@ dict({ 'SE3': list([ dict({ - 'end': '2025-07-05T23:00:00+00:00', - 'price': 43.57, - 'start': '2025-07-05T22:00:00+00:00', + 'end': '2025-09-30T23:00:00+00:00', + 'price': 523.75, + 'start': '2025-09-30T22:00:00+00:00', }), dict({ - 'end': '2025-07-06T00:00:00+00:00', - 'price': 36.47, - 'start': '2025-07-05T23:00:00+00:00', + 'end': '2025-10-01T00:00:00+00:00', + 'price': 485.95, + 'start': '2025-09-30T23:00:00+00:00', }), dict({ - 'end': '2025-07-06T01:00:00+00:00', - 'price': 35.57, - 'start': '2025-07-06T00:00:00+00:00', + 'end': '2025-10-01T01:00:00+00:00', + 'price': 504.85, + 'start': '2025-10-01T00:00:00+00:00', }), dict({ - 'end': '2025-07-06T02:00:00+00:00', - 'price': 30.73, - 'start': '2025-07-06T01:00:00+00:00', + 'end': '2025-10-01T02:00:00+00:00', + 'price': 442.07, + 'start': '2025-10-01T01:00:00+00:00', }), dict({ - 'end': '2025-07-06T03:00:00+00:00', - 'price': 32.42, - 'start': '2025-07-06T02:00:00+00:00', + 'end': '2025-10-01T03:00:00+00:00', + 'price': 496.12, + 'start': '2025-10-01T02:00:00+00:00', }), dict({ - 'end': '2025-07-06T04:00:00+00:00', - 'price': 38.73, - 'start': '2025-07-06T03:00:00+00:00', + 'end': '2025-10-01T04:00:00+00:00', + 'price': 670.85, + 'start': '2025-10-01T03:00:00+00:00', }), dict({ - 'end': '2025-07-06T05:00:00+00:00', - 'price': 42.78, - 'start': '2025-07-06T04:00:00+00:00', + 'end': '2025-10-01T05:00:00+00:00', + 'price': 1149.72, + 'start': '2025-10-01T04:00:00+00:00', }), dict({ - 'end': '2025-07-06T06:00:00+00:00', - 'price': 54.71, - 'start': '2025-07-06T05:00:00+00:00', + 'end': '2025-10-01T06:00:00+00:00', + 'price': 1694.25, + 'start': '2025-10-01T05:00:00+00:00', }), dict({ - 'end': '2025-07-06T07:00:00+00:00', - 'price': 83.87, - 'start': '2025-07-06T06:00:00+00:00', + 'end': '2025-10-01T07:00:00+00:00', + 'price': 1378.06, + 'start': '2025-10-01T06:00:00+00:00', }), dict({ - 'end': '2025-07-06T08:00:00+00:00', - 'price': 78.8, - 'start': '2025-07-06T07:00:00+00:00', + 'end': '2025-10-01T08:00:00+00:00', + 'price': 1063.41, + 'start': '2025-10-01T07:00:00+00:00', }), dict({ - 'end': '2025-07-06T09:00:00+00:00', - 'price': 92.09, - 'start': '2025-07-06T08:00:00+00:00', + 'end': '2025-10-01T09:00:00+00:00', + 'price': 874.53, + 'start': '2025-10-01T08:00:00+00:00', }), dict({ - 'end': '2025-07-06T10:00:00+00:00', - 'price': 104.92, - 'start': '2025-07-06T09:00:00+00:00', + 'end': '2025-10-01T10:00:00+00:00', + 'price': 708.2, + 'start': '2025-10-01T09:00:00+00:00', }), dict({ - 'end': '2025-07-06T11:00:00+00:00', - 'price': 72.5, - 'start': '2025-07-06T10:00:00+00:00', + 'end': '2025-10-01T11:00:00+00:00', + 'price': 646.53, + 'start': '2025-10-01T10:00:00+00:00', }), dict({ - 'end': '2025-07-06T12:00:00+00:00', - 'price': 63.49, - 'start': '2025-07-06T11:00:00+00:00', + 'end': '2025-10-01T12:00:00+00:00', + 'price': 667.97, + 'start': '2025-10-01T11:00:00+00:00', }), dict({ - 'end': '2025-07-06T13:00:00+00:00', - 'price': 91.64, - 'start': '2025-07-06T12:00:00+00:00', + 'end': '2025-10-01T13:00:00+00:00', + 'price': 710.63, + 'start': '2025-10-01T12:00:00+00:00', }), dict({ - 'end': '2025-07-06T14:00:00+00:00', - 'price': 111.79, - 'start': '2025-07-06T13:00:00+00:00', + 'end': '2025-10-01T14:00:00+00:00', + 'price': 807.23, + 'start': '2025-10-01T13:00:00+00:00', }), dict({ - 'end': '2025-07-06T15:00:00+00:00', - 'price': 234.04, - 'start': '2025-07-06T14:00:00+00:00', + 'end': '2025-10-01T15:00:00+00:00', + 'price': 909.35, + 'start': '2025-10-01T14:00:00+00:00', }), dict({ - 'end': '2025-07-06T16:00:00+00:00', - 'price': 435.33, - 'start': '2025-07-06T15:00:00+00:00', + 'end': '2025-10-01T16:00:00+00:00', + 'price': 1388.22, + 'start': '2025-10-01T15:00:00+00:00', }), dict({ - 'end': '2025-07-06T17:00:00+00:00', - 'price': 431.84, - 'start': '2025-07-06T16:00:00+00:00', + 'end': '2025-10-01T17:00:00+00:00', + 'price': 2350.51, + 'start': '2025-10-01T16:00:00+00:00', }), dict({ - 'end': '2025-07-06T18:00:00+00:00', - 'price': 423.73, - 'start': '2025-07-06T17:00:00+00:00', + 'end': '2025-10-01T18:00:00+00:00', + 'price': 3125.13, + 'start': '2025-10-01T17:00:00+00:00', }), dict({ - 'end': '2025-07-06T19:00:00+00:00', - 'price': 437.92, - 'start': '2025-07-06T18:00:00+00:00', + 'end': '2025-10-01T19:00:00+00:00', + 'price': 1427.24, + 'start': '2025-10-01T18:00:00+00:00', }), dict({ - 'end': '2025-07-06T20:00:00+00:00', - 'price': 416.42, - 'start': '2025-07-06T19:00:00+00:00', + 'end': '2025-10-01T20:00:00+00:00', + 'price': 1056.89, + 'start': '2025-10-01T19:00:00+00:00', }), dict({ - 'end': '2025-07-06T21:00:00+00:00', - 'price': 414.39, - 'start': '2025-07-06T20:00:00+00:00', + 'end': '2025-10-01T21:00:00+00:00', + 'price': 907.69, + 'start': '2025-10-01T20:00:00+00:00', }), dict({ - 'end': '2025-07-06T22:00:00+00:00', - 'price': 396.38, - 'start': '2025-07-06T21:00:00+00:00', + 'end': '2025-10-01T22:00:00+00:00', + 'price': 826.57, + 'start': '2025-10-01T21:00:00+00:00', }), ]), }) diff --git a/tests/components/nordpool/test_config_flow.py b/tests/components/nordpool/test_config_flow.py index 1f0e99b65ff97f..9756c909cf3846 100644 --- a/tests/components/nordpool/test_config_flow.py +++ b/tests/components/nordpool/test_config_flow.py @@ -26,7 +26,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") async def test_form(hass: HomeAssistant, get_client: NordPoolClient) -> None: """Test we get the form.""" @@ -48,7 +48,7 @@ async def test_form(hass: HomeAssistant, get_client: NordPoolClient) -> None: assert result["data"] == {"areas": ["SE3", "SE4"], "currency": "SEK"} -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") async def test_single_config_entry( hass: HomeAssistant, load_int: None, get_client: NordPoolClient ) -> None: @@ -61,7 +61,7 @@ async def test_single_config_entry( assert result["reason"] == "single_instance_allowed" -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") @pytest.mark.parametrize( ("error_message", "p_error"), [ @@ -107,7 +107,7 @@ async def test_cannot_connect( assert result["data"] == {"areas": ["SE3", "SE4"], "currency": "SEK"} -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") async def test_reconfigure( hass: HomeAssistant, load_int: MockConfigEntry, @@ -134,7 +134,7 @@ async def test_reconfigure( } -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") @pytest.mark.parametrize( ("error_message", "p_error"), [ diff --git a/tests/components/nordpool/test_coordinator.py b/tests/components/nordpool/test_coordinator.py index e9af70d05bc870..94d66a789cc50c 100644 --- a/tests/components/nordpool/test_coordinator.py +++ b/tests/components/nordpool/test_coordinator.py @@ -31,7 +31,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed -@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T10:00:00+00:00") async def test_coordinator( hass: HomeAssistant, get_client: NordPoolClient, @@ -50,7 +50,26 @@ async def test_coordinator( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "0.92737" + assert state.state == "0.67405" + + assert "Next data update at 2025-10-01 11:00:00+00:00" in caplog.text + assert "Next listener update at 2025-10-01 10:15:00+00:00" in caplog.text + + with ( + patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + wraps=get_client.async_get_delivery_period, + ) as mock_data, + ): + freezer.tick(timedelta(minutes=17)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + assert mock_data.call_count == 0 + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.63858" + + assert "Next data update at 2025-10-01 11:00:00+00:00" in caplog.text + assert "Next listener update at 2025-10-01 10:30:00+00:00" in caplog.text with ( patch( @@ -63,7 +82,7 @@ async def test_coordinator( await hass.async_block_till_done(wait_background_tasks=True) assert mock_data.call_count == 1 state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "0.92505" + assert state.state == "0.66068" with ( patch( @@ -77,7 +96,7 @@ async def test_coordinator( await hass.async_block_till_done(wait_background_tasks=True) assert mock_data.call_count == 1 state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "0.94949" + assert state.state == "0.68544" assert "Authentication error" in caplog.text with ( @@ -93,7 +112,7 @@ async def test_coordinator( # Empty responses does not raise assert mock_data.call_count == 3 state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "1.04203" + assert state.state == "0.72953" assert "Empty response" in caplog.text with ( @@ -108,7 +127,7 @@ async def test_coordinator( await hass.async_block_till_done(wait_background_tasks=True) assert mock_data.call_count == 1 state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "1.25889" + assert state.state == "0.90294" assert "error" in caplog.text with ( @@ -123,7 +142,7 @@ async def test_coordinator( await hass.async_block_till_done(wait_background_tasks=True) assert mock_data.call_count == 1 state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "1.81645" + assert state.state == "1.16266" assert "error" in caplog.text with ( @@ -138,14 +157,14 @@ async def test_coordinator( await hass.async_block_till_done(wait_background_tasks=True) assert mock_data.call_count == 1 state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "2.51265" + assert state.state == "1.90004" assert "Response error" in caplog.text freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "1.81983" + assert state.state == "3.42983" # Test manual polling hass.config_entries.async_update_entry( @@ -156,14 +175,14 @@ async def test_coordinator( async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "1.01177" + assert state.state == "1.42403" # Prices should update without any polling made (read from cache) freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "0.83553" + assert state.state == "1.1358" # Test manually updating the data with ( @@ -184,7 +203,7 @@ async def test_coordinator( async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "0.79619" + assert state.state == "0.933" hass.config_entries.async_update_entry( entry=config_entry, pref_disable_polling=False diff --git a/tests/components/nordpool/test_diagnostics.py b/tests/components/nordpool/test_diagnostics.py index a9dfdd5eca587a..d38e921afbbfa8 100644 --- a/tests/components/nordpool/test_diagnostics.py +++ b/tests/components/nordpool/test_diagnostics.py @@ -12,7 +12,7 @@ from tests.typing import ClientSessionGenerator -@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T10:00:00+00:00") async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/nordpool/test_init.py b/tests/components/nordpool/test_init.py index 48ddc59d083da7..e2b16d37bd63d2 100644 --- a/tests/components/nordpool/test_init.py +++ b/tests/components/nordpool/test_init.py @@ -28,7 +28,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker -@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T10:00:00+00:00") async def test_unload_entry(hass: HomeAssistant, get_client: NordPoolClient) -> None: """Test load and unload an entry.""" entry = MockConfigEntry( @@ -79,7 +79,7 @@ async def test_initial_startup_fails( assert entry.state is ConfigEntryState.SETUP_RETRY -@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T10:00:00+00:00") async def test_reconfigure_cleans_up_device( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -115,7 +115,7 @@ async def test_reconfigure_cleans_up_device( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-04", + "date": "2025-09-30", "market": "DayAhead", "deliveryArea": "NL", "currency": "EUR", @@ -126,7 +126,7 @@ async def test_reconfigure_cleans_up_device( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-05", + "date": "2025-10-01", "market": "DayAhead", "deliveryArea": "NL", "currency": "EUR", @@ -137,7 +137,7 @@ async def test_reconfigure_cleans_up_device( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-06", + "date": "2025-10-02", "market": "DayAhead", "deliveryArea": "NL", "currency": "EUR", diff --git a/tests/components/nordpool/test_sensor.py b/tests/components/nordpool/test_sensor.py index 082684a2a02b4e..cedcb57c95ecdc 100644 --- a/tests/components/nordpool/test_sensor.py +++ b/tests/components/nordpool/test_sensor.py @@ -20,7 +20,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, @@ -33,7 +33,7 @@ async def test_sensor( await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_current_price_is_0( hass: HomeAssistant, load_int: ConfigEntry @@ -43,10 +43,10 @@ async def test_sensor_current_price_is_0( current_price = hass.states.get("sensor.nord_pool_se4_current_price") assert current_price is not None - assert current_price.state == "0.0" # SE4 2024-11-05T18:00:00Z + assert current_price.state == "0.0" # SE4 2025-10-01T18:00:00Z -@pytest.mark.freeze_time("2024-11-05T23:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T21:45:00+00:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_no_next_price(hass: HomeAssistant, load_int: ConfigEntry) -> None: """Test the Nord Pool sensor.""" @@ -58,12 +58,12 @@ async def test_sensor_no_next_price(hass: HomeAssistant, load_int: ConfigEntry) assert current_price is not None assert last_price is not None assert next_price is not None - assert current_price.state == "0.12666" # SE3 2024-11-05T23:00:00Z - assert last_price.state == "0.28914" # SE3 2024-11-05T22:00:00Z - assert next_price.state == "0.07406" # SE3 2024-11-06T00:00:00Z" + assert current_price.state == "0.78568" # SE3 2025-10-01T21:45:00Z + assert last_price.state == "0.82171" # SE3 2025-10-01T21:30:00Z + assert next_price.state == "0.81174" # SE3 2025-10-01T22:00:00Z -@pytest.mark.freeze_time("2024-11-06T00:00:00+01:00") +@pytest.mark.freeze_time("2025-10-02T00:00:00+02:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_no_previous_price( hass: HomeAssistant, load_int: ConfigEntry @@ -77,12 +77,12 @@ async def test_sensor_no_previous_price( assert current_price is not None assert last_price is not None assert next_price is not None - assert current_price.state == "0.12666" # SE3 2024-11-05T23:00:00Z - assert last_price.state == "0.28914" # SE3 2024-11-05T22:00:00Z - assert next_price.state == "0.07406" # SE3 2024-11-06T00:00:00Z + assert current_price.state == "0.93322" # SE3 2025-10-01T22:00:00Z + assert last_price.state == "0.8605" # SE3 2025-10-01T21:45:00Z + assert next_price.state == "0.83513" # SE3 2025-10-01T22:15:00Z -@pytest.mark.freeze_time("2024-11-05T11:00:01+01:00") +@pytest.mark.freeze_time("2025-10-01T11:00:01+01:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_empty_response( hass: HomeAssistant, @@ -101,16 +101,16 @@ async def test_sensor_empty_response( assert current_price is not None assert last_price is not None assert next_price is not None - assert current_price.state == "0.92737" - assert last_price.state == "1.03132" - assert next_price.state == "0.92505" + assert current_price.state == "0.67405" + assert last_price.state == "0.8616" + assert next_price.state == "0.63736" aioclient_mock.clear_requests() aioclient_mock.request( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-04", + "date": "2025-09-30", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", @@ -121,7 +121,7 @@ async def test_sensor_empty_response( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-05", + "date": "2025-10-01", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", @@ -133,7 +133,7 @@ async def test_sensor_empty_response( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-06", + "date": "2025-10-02", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", @@ -153,16 +153,16 @@ async def test_sensor_empty_response( assert current_price is not None assert last_price is not None assert next_price is not None - assert current_price.state == "0.92505" - assert last_price.state == "0.92737" - assert next_price.state == "0.94949" + assert current_price.state == "0.63736" + assert last_price.state == "0.67405" + assert next_price.state == "0.62233" aioclient_mock.clear_requests() aioclient_mock.request( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-04", + "date": "2025-09-30", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", @@ -173,7 +173,7 @@ async def test_sensor_empty_response( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-05", + "date": "2025-10-01", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", @@ -185,7 +185,7 @@ async def test_sensor_empty_response( "GET", url=API + "/DayAheadPrices", params={ - "date": "2024-11-06", + "date": "2025-10-02", "market": "DayAhead", "deliveryArea": "SE3,SE4", "currency": "SEK", @@ -193,7 +193,7 @@ async def test_sensor_empty_response( status=HTTPStatus.NO_CONTENT, ) - freezer.move_to("2024-11-05T22:00:01+00:00") + freezer.move_to("2025-10-01T21:45:01+00:00") async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -206,6 +206,6 @@ async def test_sensor_empty_response( assert current_price is not None assert last_price is not None assert next_price is not None - assert current_price.state == "0.28914" - assert last_price.state == "0.5223" + assert current_price.state == "0.78568" + assert last_price.state == "0.82171" assert next_price.state == STATE_UNKNOWN diff --git a/tests/components/nordpool/test_services.py b/tests/components/nordpool/test_services.py index 1042783fee8cba..9d940af4ad77a2 100644 --- a/tests/components/nordpool/test_services.py +++ b/tests/components/nordpool/test_services.py @@ -30,31 +30,31 @@ TEST_SERVICE_DATA = { ATTR_CONFIG_ENTRY: "to_replace", - ATTR_DATE: "2024-11-05", + ATTR_DATE: "2025-10-01", ATTR_AREAS: "SE3", ATTR_CURRENCY: "EUR", } TEST_SERVICE_DATA_USE_DEFAULTS = { ATTR_CONFIG_ENTRY: "to_replace", - ATTR_DATE: "2024-11-05", + ATTR_DATE: "2025-10-01", } TEST_SERVICE_INDICES_DATA_60 = { ATTR_CONFIG_ENTRY: "to_replace", - ATTR_DATE: "2025-07-06", + ATTR_DATE: "2025-10-01", ATTR_AREAS: "SE3", ATTR_CURRENCY: "SEK", ATTR_RESOLUTION: 60, } TEST_SERVICE_INDICES_DATA_15 = { ATTR_CONFIG_ENTRY: "to_replace", - ATTR_DATE: "2025-07-06", + ATTR_DATE: "2025-10-01", ATTR_AREAS: "SE3", ATTR_CURRENCY: "SEK", ATTR_RESOLUTION: 15, } -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") async def test_service_call( hass: HomeAssistant, load_int: MockConfigEntry, @@ -96,7 +96,7 @@ async def test_service_call( (NordPoolError, "connection_error"), ], ) -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") async def test_service_call_failures( hass: HomeAssistant, load_int: MockConfigEntry, @@ -124,7 +124,7 @@ async def test_service_call_failures( assert err.value.translation_key == key -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") async def test_empty_response_returns_empty_list( hass: HomeAssistant, load_int: MockConfigEntry, @@ -151,7 +151,7 @@ async def test_empty_response_returns_empty_list( assert response == snapshot -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") async def test_service_call_config_entry_bad_state( hass: HomeAssistant, load_int: MockConfigEntry, @@ -184,7 +184,7 @@ async def test_service_call_config_entry_bad_state( assert err.value.translation_key == "entry_not_loaded" -@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00") async def test_service_call_for_price_indices( hass: HomeAssistant, load_int: MockConfigEntry, @@ -200,7 +200,7 @@ async def test_service_call_for_price_indices( "GET", url=API + "/DayAheadPriceIndices", params={ - "date": "2025-07-06", + "date": "2025-10-01", "market": "DayAhead", "indexNames": "SE3", "currency": "SEK", @@ -213,7 +213,7 @@ async def test_service_call_for_price_indices( "GET", url=API + "/DayAheadPriceIndices", params={ - "date": "2025-07-06", + "date": "2025-10-01", "market": "DayAhead", "indexNames": "SE3", "currency": "SEK", From 229ebe16f3dc28ffacdefc60e759189f596d1d25 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:36:10 -0400 Subject: [PATCH 05/33] Disable baudrate bootloader reset for ZBT-2 (#153443) --- .../components/homeassistant_connect_zbt2/config_flow.py | 6 +----- .../components/homeassistant_connect_zbt2/update.py | 2 +- .../homeassistant_connect_zbt2/test_config_flow.py | 5 +---- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homeassistant_connect_zbt2/config_flow.py b/homeassistant/components/homeassistant_connect_zbt2/config_flow.py index 34af7b6168a1a1..1d95601211e445 100644 --- a/homeassistant/components/homeassistant_connect_zbt2/config_flow.py +++ b/homeassistant/components/homeassistant_connect_zbt2/config_flow.py @@ -67,11 +67,7 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol): """Mixin for Home Assistant Connect ZBT-2 firmware methods.""" context: ConfigFlowContext - - # `rts_dtr` targets older adapters, `baudrate` works for newer ones. The reason we - # try them in this order is that on older adapters `baudrate` entered the ESP32-S3 - # bootloader instead of the MG24 bootloader. - BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE] + BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR] async def async_step_install_zigbee_firmware( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/homeassistant_connect_zbt2/update.py b/homeassistant/components/homeassistant_connect_zbt2/update.py index 6c8819a7da9659..e6d66ca822d514 100644 --- a/homeassistant/components/homeassistant_connect_zbt2/update.py +++ b/homeassistant/components/homeassistant_connect_zbt2/update.py @@ -157,7 +157,7 @@ async def async_setup_entry( class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): """Connect ZBT-2 firmware update entity.""" - bootloader_reset_methods = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE] + bootloader_reset_methods = [ResetTarget.RTS_DTR] def __init__( self, diff --git a/tests/components/homeassistant_connect_zbt2/test_config_flow.py b/tests/components/homeassistant_connect_zbt2/test_config_flow.py index 62a34bc1d3555a..dc32741165e50d 100644 --- a/tests/components/homeassistant_connect_zbt2/test_config_flow.py +++ b/tests/components/homeassistant_connect_zbt2/test_config_flow.py @@ -328,10 +328,7 @@ async def test_options_flow( # Verify async_flash_silabs_firmware was called with ZBT-2's reset methods assert flash_mock.call_count == 1 - assert flash_mock.mock_calls[0].kwargs["bootloader_reset_methods"] == [ - "rts_dtr", - "baudrate", - ] + assert flash_mock.mock_calls[0].kwargs["bootloader_reset_methods"] == ["rts_dtr"] async def test_duplicate_discovery(hass: HomeAssistant) -> None: From d20631598e57da89a54afc308e7c545a7c58f330 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 2 Oct 2025 19:44:24 +0300 Subject: [PATCH 06/33] Bump aioshelly 13.11.0 (#153458) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 7c3292f5dea3ae..5f1f767271b46d 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "silver", - "requirements": ["aioshelly==13.10.0"], + "requirements": ["aioshelly==13.11.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index bd7d10b0b120a1..25b4c56c08f5cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -384,7 +384,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.10.0 +aioshelly==13.11.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41fee2f799b727..717be00c1fca89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -366,7 +366,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.10.0 +aioshelly==13.11.0 # homeassistant.components.skybell aioskybell==22.7.0 From cd69b82fc93789cb7e9220e8dcb4bd33bda95d66 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 2 Oct 2025 19:06:53 +0200 Subject: [PATCH 07/33] Add light, security and climate panel (#153261) --- homeassistant/components/frontend/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 4bdaff92b0162c..ebd354c5e8390f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -452,6 +452,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.app.router.register_resource(IndexView(repo_path, hass)) + async_register_built_in_panel(hass, "light") + async_register_built_in_panel(hass, "security") + async_register_built_in_panel(hass, "climate") + async_register_built_in_panel(hass, "profile") async_register_built_in_panel( From f9f61b8da75b956c58927f6c583d1c650f905fe4 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 2 Oct 2025 19:22:34 +0200 Subject: [PATCH 08/33] Portainer add configuration URL's (#153466) --- homeassistant/components/portainer/entity.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/portainer/entity.py b/homeassistant/components/portainer/entity.py index 5fd53236cd8257..907e8cf4afe900 100644 --- a/homeassistant/components/portainer/entity.py +++ b/homeassistant/components/portainer/entity.py @@ -1,7 +1,9 @@ """Base class for Portainer entities.""" from pyportainer.models.docker import DockerContainer +from yarl import URL +from homeassistant.const import CONF_URL from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -31,6 +33,9 @@ def __init__( identifiers={ (DOMAIN, f"{coordinator.config_entry.entry_id}_{self.device_id}") }, + configuration_url=URL( + f"{coordinator.config_entry.data[CONF_URL]}#!/{self.device_id}/docker/dashboard" + ), manufacturer=DEFAULT_NAME, model="Endpoint", name=device_info.endpoint.name, @@ -63,6 +68,9 @@ def __init__( (DOMAIN, f"{self.coordinator.config_entry.entry_id}_{device_name}") }, manufacturer=DEFAULT_NAME, + configuration_url=URL( + f"{coordinator.config_entry.data[CONF_URL]}#!/{self.endpoint_id}/docker/containers/{self.device_id}" + ), model="Container", name=device_name, via_device=( From a2a067a81ce0245a0d4bb1048abb77540a04cbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Thu, 2 Oct 2025 19:25:19 +0200 Subject: [PATCH 09/33] Add serial number to the list of discovered devices (#153448) --- homeassistant/components/airthings_ble/config_flow.py | 2 +- tests/components/airthings_ble/test_config_flow.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py index 2d32fa6e7df04a..fa6a52a5a79834 100644 --- a/homeassistant/components/airthings_ble/config_flow.py +++ b/homeassistant/components/airthings_ble/config_flow.py @@ -171,7 +171,7 @@ async def async_step_user( return self.async_abort(reason="no_devices_found") titles = { - address: discovery.device.name + address: get_name(discovery.device) for (address, discovery) in self._discovered_devices.items() } return self.async_show_form( diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index 2adc5498e7b85f..42db22a99153ce 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -136,7 +136,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: schema = result["data_schema"].schema assert schema.get(CONF_ADDRESS).container == { - "cc:cc:cc:cc:cc:cc": "Airthings Wave Plus" + "cc:cc:cc:cc:cc:cc": "Airthings Wave Plus (123456)" } with patch( @@ -186,7 +186,7 @@ async def test_user_setup_replaces_ignored_device(hass: HomeAssistant) -> None: schema = result["data_schema"].schema assert schema.get(CONF_ADDRESS).container == { - "cc:cc:cc:cc:cc:cc": "Airthings Wave Plus" + "cc:cc:cc:cc:cc:cc": "Airthings Wave Plus (123456)" } with patch( From 3f7a28852683a7d852b8bade96c02d3970f8d4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Thu, 2 Oct 2025 19:25:59 +0200 Subject: [PATCH 10/33] Add data_description field for Airthings BLE (#153442) --- homeassistant/components/airthings_ble/strings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json index 4b38923384a2fa..f73546bbe422e6 100644 --- a/homeassistant/components/airthings_ble/strings.json +++ b/homeassistant/components/airthings_ble/strings.json @@ -6,6 +6,9 @@ "description": "[%key:component::bluetooth::config::step::user::description%]", "data": { "address": "[%key:common::config_flow::data::device%]" + }, + "data_description": { + "address": "The Airthings devices discovered via Bluetooth." } }, "bluetooth_confirm": { From 64875894d6be7463c429ab17144f77dc1b2c26d4 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 2 Oct 2025 19:27:28 +0200 Subject: [PATCH 11/33] Fix sentence-casing in user-facing strings of `slack` (#153427) --- homeassistant/components/slack/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/slack/strings.json b/homeassistant/components/slack/strings.json index 13b48644ffd78f..960ae3cccbc6aa 100644 --- a/homeassistant/components/slack/strings.json +++ b/homeassistant/components/slack/strings.json @@ -5,14 +5,14 @@ "description": "Refer to the documentation on getting your Slack API key.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", - "default_channel": "Default Channel", + "default_channel": "Default channel", "icon": "Icon", "username": "[%key:common::config_flow::data::username%]" }, "data_description": { "api_key": "The Slack API token to use for sending Slack messages.", "default_channel": "The channel to post to if no channel is specified when sending a message.", - "icon": "Use one of the Slack emojis as an Icon for the supplied username.", + "icon": "Use one of the Slack emojis as an icon for the supplied username.", "username": "Home Assistant will post to Slack using the username specified." } } From d92004a9e75e173d3c232977130c6235f5051cd3 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 2 Oct 2025 18:33:57 +0100 Subject: [PATCH 12/33] Add missing translation for media browser default title (#153430) Co-authored-by: Erwin Douna Co-authored-by: Norbert Rittel --- homeassistant/components/media_source/models.py | 6 +++++- homeassistant/components/media_source/strings.json | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 2cf5d231741ef3..ac633e8753dbc7 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -7,6 +7,7 @@ from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.translation import async_get_cached_translations from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX @@ -62,12 +63,15 @@ def media_source_id(self) -> str: async def async_browse(self) -> BrowseMediaSource: """Browse this item.""" if self.domain is None: + title = async_get_cached_translations( + self.hass, self.hass.config.language, "common", "media_source" + ).get("component.media_source.common.sources_default", "Media Sources") base = BrowseMediaSource( domain=None, identifier=None, media_class=MediaClass.APP, media_content_type=MediaType.APPS, - title="Media Sources", + title=title, can_play=False, can_expand=True, children_media_class=MediaClass.APP, diff --git a/homeassistant/components/media_source/strings.json b/homeassistant/components/media_source/strings.json index 40204fc32db9e7..12f69ad4390089 100644 --- a/homeassistant/components/media_source/strings.json +++ b/homeassistant/components/media_source/strings.json @@ -9,5 +9,8 @@ "unknown_media_source": { "message": "Unknown media source: {domain}" } + }, + "common": { + "sources_default": "Media sources" } } From 22f2f8680a980dc550f3ca1b7af2b45e37bb49b6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Oct 2025 20:16:24 +0200 Subject: [PATCH 13/33] Improve recorder migration tests dropping indices (#153456) --- tests/components/recorder/common.py | 24 +++++++++ .../recorder/test_migration_from_schema_32.py | 10 ++-- .../components/recorder/test_v32_migration.py | 49 +++++++++++++------ 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 094ab11a112ef9..0c1ad43823df27 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -16,6 +16,7 @@ from unittest.mock import MagicMock, patch, sentinel from freezegun import freeze_time +import pytest from sqlalchemy import create_engine, event as sqlalchemy_event from sqlalchemy.orm.session import Session @@ -583,3 +584,26 @@ def db_state_attributes_to_native(state_attrs: StateAttributes) -> dict[str, Any if shared_attrs is None: return {} return cast(dict[str, Any], json_loads(shared_attrs)) + + +async def async_drop_index( + recorder: Recorder, table: str, index: str, caplog: pytest.LogCaptureFixture +) -> None: + """Drop an index from the database. + + migration._drop_index does not return or raise, so we verify the result + by checking the log for success or failure messages. + """ + + finish_msg = f"Finished dropping index `{index}` from table `{table}`" + fail_msg = f"Failed to drop index `{index}` from table `{table}`" + + count_finish = caplog.text.count(finish_msg) + count_fail = caplog.text.count(fail_msg) + + await recorder.async_add_executor_job( + migration._drop_index, recorder.get_session, table, index + ) + + assert caplog.text.count(finish_msg) == count_finish + 1 + assert caplog.text.count(fail_msg) == count_fail diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 2a24b30b7f5565..5305db0db6d5f5 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -46,6 +46,7 @@ from .common import ( async_attach_db_engine, + async_drop_index, async_recorder_block_till_done, async_wait_recording_done, get_patched_live_version, @@ -132,6 +133,7 @@ def db_schema_32(): async def test_migrate_events_context_ids( async_test_recorder: RecorderInstanceContextManager, indices_to_drop: list[tuple[str, str]], + caplog: pytest.LogCaptureFixture, ) -> None: """Test we can migrate old uuid context ids and ulid context ids to binary format.""" importlib.import_module(SCHEMA_MODULE_32) @@ -257,7 +259,7 @@ def _insert_events(): for table, index in indices_to_drop: with session_scope(hass=hass) as session: assert get_index_by_name(session, table, index) is not None - migration._drop_index(instance.get_session, table, index) + await async_drop_index(instance, table, index, caplog) await hass.async_stop() await hass.async_block_till_done() @@ -534,6 +536,7 @@ def _insert_migration(): async def test_migrate_states_context_ids( async_test_recorder: RecorderInstanceContextManager, indices_to_drop: list[tuple[str, str]], + caplog: pytest.LogCaptureFixture, ) -> None: """Test we can migrate old uuid context ids and ulid context ids to binary format.""" importlib.import_module(SCHEMA_MODULE_32) @@ -637,7 +640,7 @@ def _insert_states(): for table, index in indices_to_drop: with session_scope(hass=hass) as session: assert get_index_by_name(session, table, index) is not None - migration._drop_index(instance.get_session, table, index) + await async_drop_index(instance, table, index, caplog) await hass.async_stop() await hass.async_block_till_done() @@ -1152,6 +1155,7 @@ def _fetch_migrated_states(): async def test_post_migrate_entity_ids( async_test_recorder: RecorderInstanceContextManager, indices_to_drop: list[tuple[str, str]], + caplog: pytest.LogCaptureFixture, ) -> None: """Test we can migrate entity_ids to the StatesMeta table.""" importlib.import_module(SCHEMA_MODULE_32) @@ -1207,7 +1211,7 @@ def _insert_events(): for table, index in indices_to_drop: with session_scope(hass=hass) as session: assert get_index_by_name(session, table, index) is not None - migration._drop_index(instance.get_session, table, index) + await async_drop_index(instance, table, index, caplog) await hass.async_stop() await hass.async_block_till_done() diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index ca7be224381e04..b4836fb5cde4d3 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -19,7 +19,11 @@ from homeassistant.core import Event, EventOrigin, State from homeassistant.util import dt as dt_util -from .common import async_wait_recording_done, get_patched_live_version +from .common import ( + async_drop_index, + async_wait_recording_done, + get_patched_live_version, +) from .conftest import instrument_migration from tests.common import async_test_home_assistant @@ -430,6 +434,7 @@ def _get_states_index_names(): patch.object(core, "EventData", old_db_schema.EventData), patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), + patch.object(migration, "Base", old_db_schema.Base), patch( CREATE_ENGINE_TARGET, new=_create_engine_test( @@ -455,12 +460,22 @@ def _add_data(): await hass.async_block_till_done() await instance.async_block_till_done() - await instance.async_add_executor_job( - migration._drop_index, - instance.get_session, - "states", - "ix_states_event_id", - ) + if not recorder_db_url.startswith("sqlite://"): + await instance.async_add_executor_job( + migration._drop_foreign_key_constraints, + instance.get_session, + instance.engine, + "states", + "event_id", + ) + await async_drop_index(instance, "states", "ix_states_event_id", caplog) + if not recorder_db_url.startswith("sqlite://"): + await instance.async_add_executor_job( + migration._restore_foreign_key_constraints, + instance.get_session, + instance.engine, + [("states", "event_id", "events", "event_id")], + ) states_indexes = await instance.async_add_executor_job( _get_states_index_names @@ -599,12 +614,7 @@ def _add_data(): await hass.async_block_till_done() await instance.async_block_till_done() - await instance.async_add_executor_job( - migration._drop_index, - instance.get_session, - "states", - "ix_states_event_id", - ) + await async_drop_index(instance, "states", "ix_states_event_id", caplog) states_indexes = await instance.async_add_executor_job( _get_states_index_names @@ -763,6 +773,7 @@ def _get_states_index_names(): patch.object(core, "EventData", old_db_schema.EventData), patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), + patch.object(migration, "Base", old_db_schema.Base), patch( CREATE_ENGINE_TARGET, new=_create_engine_test( @@ -789,10 +800,18 @@ def _add_data(): await instance.async_block_till_done() await instance.async_add_executor_job( - migration._drop_index, + migration._drop_foreign_key_constraints, instance.get_session, + instance.engine, "states", - "ix_states_event_id", + "event_id", + ) + await async_drop_index(instance, "states", "ix_states_event_id", caplog) + await instance.async_add_executor_job( + migration._restore_foreign_key_constraints, + instance.get_session, + instance.engine, + [("states", "event_id", "events", "event_id")], ) states_indexes = await instance.async_add_executor_job( From a7f48360b7b6655b1f9ffcaaafe95001c8913b12 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Thu, 2 Oct 2025 19:32:37 +0100 Subject: [PATCH 14/33] Add PARALLEL_UPDATES to Squeezebox switch platform (#153477) --- homeassistant/components/squeezebox/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/squeezebox/switch.py b/homeassistant/components/squeezebox/switch.py index 33926c53e649c6..f8512124068a48 100644 --- a/homeassistant/components/squeezebox/switch.py +++ b/homeassistant/components/squeezebox/switch.py @@ -22,6 +22,8 @@ _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 + async def async_setup_entry( hass: HomeAssistant, From 571b2e3ab6df20b841669e27119c47223bfae5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Thu, 2 Oct 2025 20:38:27 +0200 Subject: [PATCH 15/33] Fix Airthings config flow description (#153452) --- homeassistant/components/airthings/config_flow.py | 15 +++++++++------ homeassistant/components/airthings/strings.json | 6 +++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/airthings/config_flow.py b/homeassistant/components/airthings/config_flow.py index 23711b7a9a20b9..42e21b2846734b 100644 --- a/homeassistant/components/airthings/config_flow.py +++ b/homeassistant/components/airthings/config_flow.py @@ -23,6 +23,10 @@ } ) +URL_API_INTEGRATION = { + "url": "https://dashboard.airthings.com/integrations/api-integration" +} + class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Airthings.""" @@ -37,11 +41,7 @@ async def async_step_user( return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, - description_placeholders={ - "url": ( - "https://dashboard.airthings.com/integrations/api-integration" - ), - }, + description_placeholders=URL_API_INTEGRATION, ) errors = {} @@ -65,5 +65,8 @@ async def async_step_user( return self.async_create_entry(title="Airthings", data=user_input) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + description_placeholders=URL_API_INTEGRATION, ) diff --git a/homeassistant/components/airthings/strings.json b/homeassistant/components/airthings/strings.json index 610891fff10439..4135e3fd387b73 100644 --- a/homeassistant/components/airthings/strings.json +++ b/homeassistant/components/airthings/strings.json @@ -4,9 +4,9 @@ "user": { "data": { "id": "ID", - "secret": "Secret", - "description": "Login at {url} to find your credentials" - } + "secret": "Secret" + }, + "description": "Login at {url} to find your credentials" } }, "error": { From d2aa0573ded1612d313a6676bc3d520bd49b1df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ebbinghaus?= Date: Thu, 2 Oct 2025 20:44:19 +0200 Subject: [PATCH 16/33] Add relative humidity to matter climate entities (#152554) OK after talking with Marcel. --- homeassistant/components/matter/climate.py | 14 ++ homeassistant/components/matter/sensor.py | 1 + tests/components/matter/conftest.py | 1 + .../tado_smart_radiator_thermostat_x.json | 198 ++++++++++++++++++ .../matter/snapshots/test_button.ambr | 49 +++++ .../matter/snapshots/test_climate.ambr | 65 ++++++ .../matter/snapshots/test_sensor.ambr | 109 ++++++++++ tests/components/matter/test_climate.py | 53 +++++ 8 files changed, 490 insertions(+) create mode 100644 tests/components/matter/fixtures/nodes/tado_smart_radiator_thermostat_x.json diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index c15dd42d62b672..4b28fe7625b4a1 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -30,6 +30,7 @@ from .helpers import get_matter from .models import MatterDiscoverySchema +HUMIDITY_SCALING_FACTOR = 100 TEMPERATURE_SCALING_FACTOR = 100 HVAC_SYSTEM_MODE_MAP = { HVACMode.OFF: 0, @@ -261,6 +262,18 @@ def _update_from_device(self) -> None: self._attr_current_temperature = self._get_temperature_in_degrees( clusters.Thermostat.Attributes.LocalTemperature ) + + self._attr_current_humidity = ( + int(raw_measured_humidity) / HUMIDITY_SCALING_FACTOR + if ( + raw_measured_humidity := self.get_matter_attribute_value( + clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue + ) + ) + is not None + else None + ) + if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False: # special case: the appliance has a dedicated Power switch on the OnOff cluster # if the mains power is off - treat it as if the HVAC mode is off @@ -428,6 +441,7 @@ def _get_temperature_in_degrees( clusters.Thermostat.Attributes.TemperatureSetpointHold, clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint, clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint, + clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue, clusters.OnOff.Attributes.OnOff, ), device_type=(device_types.Thermostat, device_types.RoomAirConditioner), diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index b8249e9efa3aec..0c95cda947426d 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -351,6 +351,7 @@ def _update_from_device(self) -> None: required_attributes=( clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue, ), + allow_multi=True, # also used for climate entity ), MatterDiscoverySchema( platform=Platform.SENSOR, diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index dca29cd7abd632..9b82f2ac305592 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -121,6 +121,7 @@ async def integration_fixture( "smoke_detector", "solar_power", "switch_unit", + "tado_smart_radiator_thermostat_x", "temperature_sensor", "thermostat", "vacuum_cleaner", diff --git a/tests/components/matter/fixtures/nodes/tado_smart_radiator_thermostat_x.json b/tests/components/matter/fixtures/nodes/tado_smart_radiator_thermostat_x.json new file mode 100644 index 00000000000000..9111ffd03fe66c --- /dev/null +++ b/tests/components/matter/fixtures/nodes/tado_smart_radiator_thermostat_x.json @@ -0,0 +1,198 @@ +{ + "node_id": 12, + "date_commissioned": "2024-11-30T14:42:32.255793", + "last_interview": "2025-09-02T11:11:02.931246", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 48, 49, 51, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 4 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 1, + "0/40/1": "tado\u00b0 GmbH", + "0/40/2": 4942, + "0/40/3": "Smart Radiator Thermostat X", + "0/40/4": 1, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "VA04", + "0/40/9": 64, + "0/40/10": "1.0", + "0/40/18": "86A085E50D5A98E9", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 18, 19, 65528, 65529, 65531, 65532, + 65533 + ], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "DghqP9mExis=", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "DghqP9mExis=", + "0/49/7": null, + "0/49/65532": 2, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "0": "ieee802154", + "1": true, + "2": null, + "3": null, + "4": "JgVorK4gwNo=", + "5": [], + "6": [ + "/cSCg76PeeDU8k9/8VDoCg==", + "/oAAAAAAAAAkBWisriDA2g==", + "/YyzDI0GAAEI590S93bZ+g==" + ], + "7": 4 + } + ], + "0/51/1": 23, + "0/51/2": 110, + "0/51/3": 6840, + "0/51/4": 1, + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65531": [0, 1, 2, 3, 4, 8, 65528, 65529, 65531, 65532, 65533], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [], + "0/62/1": [], + "0/62/2": 5, + "0/62/3": 5, + "0/62/4": [ + "FTABAQAkAgE3AycU3mS65o4n65AmFdZw72wYJgQxwoAuJAUANwYnFN5kuuaOJ+uQJhXWcO9sGCQHASQIATAJQQQdNLSJLh6Ew+9dc42ZSEaQD2i1mavRjPh7ERTyLn8CmfJWgG9s4LZKLdh1Qu5gz5wiKQtzQwLmvjEVyMbO7YwDNwo1ASkBGCQCYDAEFG7exdou0CWA9KDmSWy1OVdhMBKHMAUUbt7F2i7QJYD0oOZJbLU5V2EwEocYMAtAF3IcZnJT290miGeEgwDYwxCO383N3BO+F5ESozS503RetTDlxunlA1cPDTKdyPRksfD14zu5erZ51aPKHxa2Qhg=", + "FTABAQAkAgE3AycUi1H2tJ00+fUkFQEYJgRfkd0uJAUANwYnFItR9rSdNPn1JBUBGCQHASQIATAJQQS9bdXZ/ocAnGmFJBkbm6+buMcdLgy3kQnyiIJ0gPArOweblS5eFfXnRSBWP7QcV7Nd7yiAUNncF+0kMrbpjEX+Nwo1ASkBGCQCYDAEFON8FiGqis2G9n3okV7J/BquBFbUMAUU43wWIaqKzYb2feiRXsn8Gq4EVtQYMAtAVYvBt/DVrSHJdjHZ7Spdtn3amDLOsTNzjsQcBOyESjCH43ZsgKQXmgqSXh+DS4qBNJm0eVo+Vn2gbhOlqubYMBg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEUVnmOqdwGAsJNKvBP6t8dNPIV8vb+7vMEdmLTlDtli9YsaJCIhfOAGWRQROt8++O953j/fnjmO6BiAKctAnrxTcKNQEpARgkAmAwBBQrF7Zs6XmGG6lbxviD1v3sViKTrDAFFCsXtmzpeYYbqVvG+IPW/exWIpOsGDALQOe8gq02WhNZYr3kUdGqSKmcl1yFgBY80ebOduJb4lzLWgCq527c8xUZjxx4fFsP9A/K8GqHwQ3mZ2+5/riGunsY", + "FTABAQEkAgE3AyyEAlVTLAcGR29vZ2xlLAELTWF0dGVyIFJvb3QnFAEAAAD+////GCYEf9JDKSYFf5Rb5TcGLIQCVVMsBwZHb29nbGUsAQtNYXR0ZXIgUm9vdCcUAQAAAP7///8YJAcBJAgBMAlBBFs332VJwg3I1yKmuKy2YKinZM57r2xsIk9+6ENJaErX2An/ZQAz0VJ9zx+6rGqcOti0HtrJCfe1x2D9VCyJI3U3CjUBKQEkAgEYJAJgMAQUcsIB91cZE7NIygDKe0X0d0ZoyX4wBRRywgH3VxkTs0jKAMp7RfR3RmjJfhgwC0BlFksWat/xjBVhCozpG9cD6cH2d7cRzhM1BRUt8NoVERZ1rFWRzueGhRzdnv2tKWZ0vryyo6Mgm83nswnbVSxvGA==", + "FTABAQAkAgE3AyYU4K5SDiYVI+Px/RgmBFfPHS8kBQA3BiYU4K5SDiYVI+Px/RgkBwEkCAEwCUEE/TWWQD6IXIqrlp/p0JaU1cWtFS88ERh82o2TP6qME9opV5HUntiUCAhRLHnIWtYZ4pubaOWUFoIp61NEP7tuUDcKNQEpARgkAmAwBBQ6xz8FGl9kRhSgC0R+nqgacfJGiDAFFDrHPwUaX2RGFKALRH6eqBpx8kaIGDALQLo8R2G//5ZeXJcE5MQ3YbJ0AJl0Ik97fKD6i/Kx2aGK2oumz3pyAsWd4gVWQxShlFdhoBhv27/HxvP3C9U++k0Y" + ], + "0/62/5": 4, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 4, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 769, + "1": 2 + } + ], + "1/29/1": [3, 29, 513, 1029], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/513/0": 2090, + "1/513/3": 500, + "1/513/4": 3000, + "1/513/18": 1800, + "1/513/27": 2, + "1/513/28": 0, + "1/513/65532": 1, + "1/513/65533": 5, + "1/513/65528": [], + "1/513/65529": [0], + "1/513/65531": [0, 3, 4, 18, 27, 28, 65528, 65529, 65531, 65532, 65533], + "1/1029/0": 7492, + "1/1029/1": 0, + "1/1029/2": 10000, + "1/1029/65532": 0, + "1/1029/65533": 3, + "1/1029/65528": [], + "1/1029/65529": [], + "1/1029/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_button.ambr b/tests/components/matter/snapshots/test_button.ambr index 39c8f66dfd9502..c16f66a5e88bfe 100644 --- a/tests/components/matter/snapshots/test_button.ambr +++ b/tests/components/matter/snapshots/test_button.ambr @@ -2282,6 +2282,55 @@ 'state': 'unknown', }) # --- +# name: test_buttons[tado_smart_radiator_thermostat_x][button.smart_radiator_thermostat_x_identify-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.smart_radiator_thermostat_x_identify', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Identify', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-IdentifyButton-3-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[tado_smart_radiator_thermostat_x][button.smart_radiator_thermostat_x_identify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'identify', + 'friendly_name': 'Smart Radiator Thermostat X Identify', + }), + 'context': , + 'entity_id': 'button.smart_radiator_thermostat_x_identify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[temperature_sensor][button.mock_temperature_sensor_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_climate.ambr b/tests/components/matter/snapshots/test_climate.ambr index 07a5a69d801aa6..f0745bfe50ca6a 100644 --- a/tests/components/matter/snapshots/test_climate.ambr +++ b/tests/components/matter/snapshots/test_climate.ambr @@ -199,6 +199,71 @@ 'state': 'off', }) # --- +# name: test_climates[tado_smart_radiator_thermostat_x][climate.smart_radiator_thermostat_x-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.smart_radiator_thermostat_x', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-MatterThermostat-513-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_climates[tado_smart_radiator_thermostat_x][climate.smart_radiator_thermostat_x-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 74.92, + 'current_temperature': 20.9, + 'friendly_name': 'Smart Radiator Thermostat X', + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + 'supported_features': , + 'temperature': 18.0, + }), + 'context': , + 'entity_id': 'climate.smart_radiator_thermostat_x', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_climates[thermostat][climate.longan_link_hvac-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 911ea0049952f3..1f3fc5b0a35691 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -6791,6 +6791,115 @@ 'state': '234.899', }) # --- +# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_radiator_thermostat_x_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-HumiditySensor-1029-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Smart Radiator Thermostat X Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smart_radiator_thermostat_x_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '74.92', + }) +# --- +# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_radiator_thermostat_x_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-ThermostatLocalTemperature-513-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[tado_smart_radiator_thermostat_x][sensor.smart_radiator_thermostat_x_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Smart Radiator Thermostat X Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_radiator_thermostat_x_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.9', + }) +# --- # name: test_sensors[temperature_sensor][sensor.mock_temperature_sensor_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index a887ce1b5df6ec..4e9afb4e6969d8 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -162,6 +162,59 @@ async def test_thermostat_base( assert state.attributes["temperature"] == 20 +@pytest.mark.parametrize("node_fixture", ["thermostat"]) +async def test_thermostat_humidity( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test thermostat humidity attribute and state updates.""" + # test entity attributes + state = hass.states.get("climate.longan_link_hvac") + assert state + + measured_value = clusters.RelativeHumidityMeasurement.Attributes.MeasuredValue + + # test current humidity update from device + set_node_attribute( + matter_node, + 1, + measured_value.cluster_id, + measured_value.attribute_id, + 1234, + ) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["current_humidity"] == 12.34 + + # test current humidity update from device with zero value + set_node_attribute( + matter_node, + 1, + measured_value.cluster_id, + measured_value.attribute_id, + 0, + ) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["current_humidity"] == 0.0 + + # test current humidity update from device with None value + set_node_attribute( + matter_node, + 1, + measured_value.cluster_id, + measured_value.attribute_id, + None, + ) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert "current_humidity" not in state.attributes + + @pytest.mark.parametrize("node_fixture", ["thermostat"]) async def test_thermostat_service_calls( hass: HomeAssistant, From 4011d62ac7f3f54c7eb5d682623415271601c252 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Oct 2025 21:03:26 +0200 Subject: [PATCH 17/33] Improve enable_migrate_event_ids recorder test fixture (#153470) Co-authored-by: J. Nick Koston --- .../recorder/test_migration_from_schema_32.py | 20 ------------------- .../components/recorder/test_v32_migration.py | 2 ++ tests/conftest.py | 12 ++++++----- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 5305db0db6d5f5..6554bb57183ef2 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -238,7 +238,6 @@ def _insert_events(): get_patched_live_version(old_db_schema), ), patch.object(migration.EventsContextIDMigration, "migrate_data"), - patch.object(migration.EventIDPostMigration, "migrate_data"), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), ): async with ( @@ -296,7 +295,6 @@ def _fetch_migrated_events(): patch( "sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create ) as wrapped_idx_create, - patch.object(migration.EventIDPostMigration, "migrate_data"), ): # Stall migration when the last non-live schema migration is done instrumented_migration.stall_on_schema_version = ( @@ -450,13 +448,6 @@ def _insert_migration(): get_patched_live_version(old_db_schema), ), patch.object(migration.EventsContextIDMigration, "migrate_data"), - patch.object( - migration.EventIDPostMigration, - "needs_migrate_impl", - return_value=migration.DataMigrationStatus( - needs_migrate=False, migration_done=True - ), - ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), ): async with ( @@ -623,7 +614,6 @@ def _insert_states(): get_patched_live_version(old_db_schema), ), patch.object(migration.StatesContextIDMigration, "migrate_data"), - patch.object(migration.EventIDPostMigration, "migrate_data"), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), ): async with ( @@ -676,7 +666,6 @@ def _fetch_migrated_states(): patch( "sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create ) as wrapped_idx_create, - patch.object(migration.EventIDPostMigration, "migrate_data"), ): # Stall migration when the last non-live schema migration is done instrumented_migration.stall_on_schema_version = ( @@ -834,13 +823,6 @@ def _insert_migration(): get_patched_live_version(old_db_schema), ), patch.object(migration.StatesContextIDMigration, "migrate_data"), - patch.object( - migration.EventIDPostMigration, - "needs_migrate_impl", - return_value=migration.DataMigrationStatus( - needs_migrate=False, migration_done=True - ), - ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), ): async with ( @@ -1194,7 +1176,6 @@ def _insert_events(): ), patch.object(migration.EntityIDMigration, "migrate_data"), patch.object(migration.EntityIDPostMigration, "migrate_data"), - patch.object(migration.EventIDPostMigration, "migrate_data"), patch(CREATE_ENGINE_TARGET, new=_create_engine_test), ): async with ( @@ -1232,7 +1213,6 @@ def _fetch_migrated_states(): patch( "sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create ) as wrapped_idx_create, - patch.object(migration.EventIDPostMigration, "migrate_data"), ): # Stall migration when the last non-live schema migration is done instrumented_migration.stall_on_schema_version = ( diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index b4836fb5cde4d3..648bb03fa3e253 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -74,6 +74,7 @@ def _create_engine_test(*args, **kwargs): @pytest.mark.parametrize("enable_migrate_state_context_ids", [True]) @pytest.mark.parametrize("enable_migrate_event_type_ids", [True]) @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) +@pytest.mark.parametrize("enable_migrate_event_ids", [True]) @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_times( @@ -246,6 +247,7 @@ def _get_events_index_names(): @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) +@pytest.mark.parametrize("enable_migrate_event_ids", [True]) @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_can_resume_entity_id_post_migration( diff --git a/tests/conftest.py b/tests/conftest.py index 50bf0c40e10f53..205396a5d949b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1659,10 +1659,12 @@ def debug_session_scope( migrate_entity_ids = ( migration.EntityIDMigration.migrate_data if enable_migrate_entity_ids else None ) - legacy_event_id_foreign_key_exists = ( - migration.EventIDPostMigration._legacy_event_id_foreign_key_exists + post_migrate_event_ids = ( + migration.EventIDPostMigration.needs_migrate_impl if enable_migrate_event_ids - else lambda _: None + else lambda _1, _2, _3: migration.DataMigrationStatus( + needs_migrate=False, migration_done=True + ) ) with ( patch( @@ -1701,8 +1703,8 @@ def debug_session_scope( autospec=True, ), patch( - "homeassistant.components.recorder.migration.EventIDPostMigration._legacy_event_id_foreign_key_exists", - side_effect=legacy_event_id_foreign_key_exists, + "homeassistant.components.recorder.migration.EventIDPostMigration.needs_migrate_impl", + side_effect=post_migrate_event_ids, autospec=True, ), patch( From aed2d3899dee333a6124a8a34b2ff7cf03186799 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 2 Oct 2025 20:04:16 +0100 Subject: [PATCH 18/33] Update OVOEnergy to 3.0.1 (#153476) --- homeassistant/components/ovo_energy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 0fc90808bc95c8..da6fb5232f71ba 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["ovoenergy"], - "requirements": ["ovoenergy==2.0.1"] + "requirements": ["ovoenergy==3.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 25b4c56c08f5cc..1db97bc2fbc5f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1670,7 +1670,7 @@ orvibo==1.1.2 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==2.0.1 +ovoenergy==3.0.1 # homeassistant.components.p1_monitor p1monitor==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 717be00c1fca89..bb29a366a27743 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1420,7 +1420,7 @@ oralb-ble==0.17.6 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==2.0.1 +ovoenergy==3.0.1 # homeassistant.components.p1_monitor p1monitor==3.2.0 From 95198ae54095cf7eca323d9dcc4a81f38b0e645d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 2 Oct 2025 21:04:32 +0200 Subject: [PATCH 19/33] Bump pyTibber to 0.32.2 (#153484) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 34f1e8fe1f908b..0844915daa4b5c 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tibber", "iot_class": "cloud_polling", "loggers": ["tibber"], - "requirements": ["pyTibber==0.32.1"] + "requirements": ["pyTibber==0.32.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1db97bc2fbc5f9..8231fabe0062e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1845,7 +1845,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.32.1 +pyTibber==0.32.2 # homeassistant.components.dlink pyW215==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb29a366a27743..8c680d74e1e99c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1556,7 +1556,7 @@ pyHomee==1.3.8 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.32.1 +pyTibber==0.32.2 # homeassistant.components.dlink pyW215==0.8.0 From 275e9485e9f074132c12cc17d8e945d4cf2f66a6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 2 Oct 2025 22:08:48 +0200 Subject: [PATCH 20/33] Fix missing powerconsumptionreport in Smartthings (#153438) --- .../components/smartthings/sensor.py | 7 +- tests/components/smartthings/conftest.py | 1 + .../device_status/tesla_powerwall.json | 107 ++++++++++++++++++ .../fixtures/devices/tesla_powerwall.json | 103 +++++++++++++++++ .../smartthings/snapshots/test_init.ambr | 31 +++++ .../smartthings/snapshots/test_sensor.ambr | 50 ++++++++ 6 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 tests/components/smartthings/fixtures/device_status/tesla_powerwall.json create mode 100644 tests/components/smartthings/fixtures/devices/tesla_powerwall.json diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index d3e2ab09a3fdf9..42581a2807e581 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -1151,8 +1151,11 @@ async def async_setup_entry( ) and ( not description.exists_fn - or description.exists_fn( - device.status[MAIN][capability][attribute] + or ( + component == MAIN + and description.exists_fn( + device.status[MAIN][capability][attribute] + ) ) ) and ( diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index c45417122e92dc..a68bbba22d215b 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -166,6 +166,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "hw_q80r_soundbar", "gas_meter", "lumi", + "tesla_powerwall", ] ) def device_fixture( diff --git a/tests/components/smartthings/fixtures/device_status/tesla_powerwall.json b/tests/components/smartthings/fixtures/device_status/tesla_powerwall.json new file mode 100644 index 00000000000000..c9531314d5fee4 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/tesla_powerwall.json @@ -0,0 +1,107 @@ +{ + "components": { + "charge": { + "powerMeter": { + "power": { + "value": 0, + "unit": "W", + "timestamp": "2025-10-02T12:21:29.196Z" + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "start": "2025-10-02T12:20:00Z", + "end": "2025-10-02T12:25:00Z", + "energy": 29765947, + "deltaEnergy": 0 + }, + "timestamp": "2025-10-02T12:26:24.729Z" + } + } + }, + "discharge": { + "powerMeter": { + "power": { + "value": 0, + "unit": "W", + "timestamp": "2025-10-02T11:41:20.556Z" + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "start": "2025-10-02T12:20:00Z", + "end": "2025-10-02T12:25:00Z", + "energy": 27827062, + "deltaEnergy": 0 + }, + "timestamp": "2025-10-02T12:26:24.729Z" + } + } + }, + "main": { + "healthCheck": { + "checkInterval": { + "value": 60, + "unit": "s", + "data": { + "deviceScheme": "UNTRACKED", + "protocol": "cloud" + }, + "timestamp": "2025-10-02T11:56:25.223Z" + }, + "healthStatus": { + "value": null + }, + "DeviceWatch-Enroll": { + "value": null + }, + "DeviceWatch-DeviceStatus": { + "value": "online", + "data": {}, + "timestamp": "2025-10-02T11:56:25.223Z" + } + }, + "rivertalent14263.adaptiveEnergyUsageState": { + "stormWatchEnabled": { + "value": true, + "timestamp": "2024-07-16T12:40:19.190Z" + }, + "stormWatchActive": { + "value": false, + "timestamp": "2024-07-16T12:40:19.190Z" + }, + "gridStatusSupport": { + "value": true, + "timestamp": "2024-07-16T12:40:19.190Z" + }, + "stormWatchSupport": { + "value": true, + "timestamp": "2025-09-17T18:31:31.669Z" + }, + "energyUsageState": { + "value": null + }, + "gridStatusStatus": { + "value": "on-grid", + "timestamp": "2025-09-17T18:31:31.669Z" + } + }, + "refresh": {}, + "battery": { + "quantity": { + "value": null + }, + "battery": { + "value": 35, + "unit": "%", + "timestamp": "2025-10-02T11:41:20.556Z" + }, + "type": { + "value": null + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/tesla_powerwall.json b/tests/components/smartthings/fixtures/devices/tesla_powerwall.json new file mode 100644 index 00000000000000..20e11a353554bf --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/tesla_powerwall.json @@ -0,0 +1,103 @@ +{ + "items": [ + { + "deviceId": "d2595c45-df6e-41ac-a7af-8e275071c19b", + "name": "UDHN-TESLA-ENERGY-BATTERY", + "label": "Powerwall", + "manufacturerName": "0AHI", + "presentationId": "STES-1-PV-TESLA-ENERGY-BATTERY", + "locationId": "d22d6401-6070-4928-8e7b-b724e2dbf425", + "ownerId": "35445a41-3ae2-4bc0-6f51-31705de6b96f", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "healthCheck", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "rivertalent14263.adaptiveEnergyUsageState", + "version": 1 + }, + { + "id": "battery", + "version": 1 + } + ], + "categories": [ + { + "name": "Battery", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "discharge", + "label": "discharge", + "capabilities": [ + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "powerMeter", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "charge", + "label": "charge", + "capabilities": [ + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "powerMeter", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2024-07-16T12:40:18.632Z", + "profile": { + "id": "4f9998dc-e672-4baf-8521-5e9b853fc978" + }, + "app": { + "installedAppId": "e798c0a6-3e3b-4299-8463-438fc3f1e6b3", + "externalId": "TESLABATTERY_1689188152863574", + "profile": { + "id": "4f9998dc-e672-4baf-8521-5e9b853fc978" + } + }, + "type": "ENDPOINT_APP", + "restrictionTier": 0, + "allowed": null, + "executionContext": "CLOUD", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 5cd56c316839cc..42eaf548b36dd1 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -1893,6 +1893,37 @@ 'via_device_id': None, }) # --- +# name: test_devices[tesla_powerwall] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + 'd2595c45-df6e-41ac-a7af-8e275071c19b', + ), + }), + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'Powerwall', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[tplink_p110] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 9e83fdacab9103..c573ccbbc27a39 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -13969,6 +13969,56 @@ 'state': '20', }) # --- +# name: test_all_entities[tesla_powerwall][sensor.powerwall_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.powerwall_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'd2595c45-df6e-41ac-a7af-8e275071c19b_main_battery_battery_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[tesla_powerwall][sensor.powerwall_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Powerwall Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.powerwall_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35', + }) +# --- # name: test_all_entities[tplink_p110][sensor.spulmaschine_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 2169ce1722937f819a27854fbb97d5b9e4468cfa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 2 Oct 2025 22:14:51 +0200 Subject: [PATCH 21/33] Remove state attributes from Firefly 3 (#153285) --- homeassistant/components/firefly_iii/sensor.py | 9 --------- tests/components/firefly_iii/snapshots/test_sensor.ambr | 9 --------- 2 files changed, 18 deletions(-) diff --git a/homeassistant/components/firefly_iii/sensor.py b/homeassistant/components/firefly_iii/sensor.py index f73238d7b2e164..e6facfb6b948bf 100644 --- a/homeassistant/components/firefly_iii/sensor.py +++ b/homeassistant/components/firefly_iii/sensor.py @@ -100,15 +100,6 @@ def native_value(self) -> str | None: """Return the state of the sensor.""" return self._account.attributes.current_balance - @property - def extra_state_attributes(self) -> dict[str, str] | None: - """Return extra state attributes for the account entity.""" - return { - "account_role": self._account.attributes.account_role or "", - "account_type": self._account.attributes.type or "", - "current_balance": str(self._account.attributes.current_balance or ""), - } - class FireflyCategoryEntity(FireflyBaseEntity, SensorEntity): """Entity for Firefly III category.""" diff --git a/tests/components/firefly_iii/snapshots/test_sensor.ambr b/tests/components/firefly_iii/snapshots/test_sensor.ambr index d381462e65a876..bccd54746ec67b 100644 --- a/tests/components/firefly_iii/snapshots/test_sensor.ambr +++ b/tests/components/firefly_iii/snapshots/test_sensor.ambr @@ -39,9 +39,6 @@ # name: test_all_entities[sensor.firefly_iii_test_credit_card-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'account_role': 'creditCard', - 'account_type': 'liability', - 'current_balance': '-250.00', 'device_class': 'monetary', 'friendly_name': 'Firefly III test Credit Card', 'icon': 'mdi:hand-coin', @@ -149,9 +146,6 @@ # name: test_all_entities[sensor.firefly_iii_test_my_checking_account-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'account_role': 'defaultAsset', - 'account_type': 'asset', - 'current_balance': '123.45', 'device_class': 'monetary', 'friendly_name': 'Firefly III test My checking account', 'icon': 'mdi:account-cash', @@ -206,9 +200,6 @@ # name: test_all_entities[sensor.firefly_iii_test_savings_account-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'account_role': 'savingsAsset', - 'account_type': 'expense', - 'current_balance': '5000.00', 'device_class': 'monetary', 'friendly_name': 'Firefly III test Savings Account', 'icon': 'mdi:cash-minus', From 3bf995eb71ffcf78d5d20d3e4685b0d0ccca2bcd Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 2 Oct 2025 22:17:11 +0200 Subject: [PATCH 22/33] Fix next event in workday calendar (#153465) --- homeassistant/components/workday/calendar.py | 10 ++++++---- tests/components/workday/test_calendar.py | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/workday/calendar.py b/homeassistant/components/workday/calendar.py index 82f2942d1f9335..e631ebb6e6a323 100644 --- a/homeassistant/components/workday/calendar.py +++ b/homeassistant/components/workday/calendar.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util import dt as dt_util from . import WorkdayConfigEntry from .const import CONF_EXCLUDES, CONF_OFFSET, CONF_WORKDAYS @@ -87,11 +88,12 @@ def update_data(self, now: datetime) -> None: @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" - return ( - sorted(self.event_list, key=lambda e: e.start)[0] - if self.event_list - else None + sorted_list: list[CalendarEvent] | None = ( + sorted(self.event_list, key=lambda e: e.start) if self.event_list else None ) + if not sorted_list: + return None + return [d for d in sorted_list if d.start >= dt_util.utcnow().date()][0] async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime diff --git a/tests/components/workday/test_calendar.py b/tests/components/workday/test_calendar.py index 5e5417362a34fb..6aa454c860f0ee 100644 --- a/tests/components/workday/test_calendar.py +++ b/tests/components/workday/test_calendar.py @@ -70,6 +70,11 @@ async def test_holiday_calendar_entity( async_fire_time_changed(hass) await hass.async_block_till_done() + # Binary sensor added to ensure same state for both entities + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == "on" + state = hass.states.get("calendar.workday_sensor_calendar") assert state is not None assert state.state == "on" @@ -78,6 +83,10 @@ async def test_holiday_calendar_entity( async_fire_time_changed(hass) await hass.async_block_till_done() + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == "off" + state = hass.states.get("calendar.workday_sensor_calendar") assert state is not None assert state.state == "off" From 3491bb1b400c59d744d113ecc7bed83b965d317c Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 2 Oct 2025 22:17:56 +0200 Subject: [PATCH 23/33] Fix missing parameter pass in onedrive (#153478) --- homeassistant/components/onedrive/backup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onedrive/backup.py b/homeassistant/components/onedrive/backup.py index bea1edce692e21..4243a920fe5702 100644 --- a/homeassistant/components/onedrive/backup.py +++ b/homeassistant/components/onedrive/backup.py @@ -163,7 +163,10 @@ async def async_upload_backup( ) try: backup_file = await LargeFileUploadClient.upload( - self._token_function, file, session=async_get_clientsession(self._hass) + self._token_function, + file, + upload_chunk_size=UPLOAD_CHUNK_SIZE, + session=async_get_clientsession(self._hass), ) except HashMismatchError as err: raise BackupAgentError( From d66da0c10d99a7832a1ec467d945b8e587b32177 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Oct 2025 22:20:45 +0200 Subject: [PATCH 24/33] Respect filtering of WS subscribe_entities when there are unserializalizable states (#153262) --- .../components/websocket_api/commands.py | 4 ++ .../components/websocket_api/test_commands.py | 46 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index d69a8c35c4f90a..a15d63b31e657a 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -473,6 +473,10 @@ def handle_subscribe_entities( serialized_states = [] for state in states: + if entity_ids and state.entity_id not in entity_ids: + continue + if entity_filter and not entity_filter(state.entity_id): + continue try: serialized_states.append(state.as_compressed_state_json) except (ValueError, TypeError): diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 07a433754ff03e..43a4fb0e539311 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1433,28 +1433,50 @@ async def test_subscribe_unsubscribe_entities( } +@pytest.mark.parametrize("unserializable_states", [[], ["light.cannot_serialize"]]) async def test_subscribe_unsubscribe_entities_specific_entities( hass: HomeAssistant, websocket_client: MockHAClientWebSocket, hass_admin_user: MockUser, + unserializable_states: list[str], ) -> None: """Test subscribe/unsubscribe entities with a list of entity ids.""" + class CannotSerializeMe: + """Cannot serialize this.""" + + def __init__(self) -> None: + """Init cannot serialize this.""" + + for entity_id in unserializable_states: + hass.states.async_set( + entity_id, + "off", + {"color": "red", "cannot_serialize": CannotSerializeMe()}, + ) + hass.states.async_set("light.permitted", "off", {"color": "red"}) - hass.states.async_set("light.not_intrested", "off", {"color": "blue"}) + hass.states.async_set("light.not_interested", "off", {"color": "blue"}) original_state = hass.states.get("light.permitted") assert isinstance(original_state, State) hass_admin_user.groups = [] hass_admin_user.mock_policy( { "entities": { - "entity_ids": {"light.permitted": True, "light.not_intrested": True} + "entity_ids": { + "light.permitted": True, + "light.not_interested": True, + "light.cannot_serialize": True, + } } } ) await websocket_client.send_json_auto_id( - {"type": "subscribe_entities", "entity_ids": ["light.permitted"]} + { + "type": "subscribe_entities", + "entity_ids": ["light.permitted", "light.cannot_serialize"], + } ) msg = await websocket_client.receive_json() @@ -1476,7 +1498,7 @@ async def test_subscribe_unsubscribe_entities_specific_entities( } } } - hass.states.async_set("light.not_intrested", "on", {"effect": "help"}) + hass.states.async_set("light.not_interested", "on", {"effect": "help"}) hass.states.async_set("light.not_permitted", "on") hass.states.async_set("light.permitted", "on", {"color": "blue"}) @@ -1497,12 +1519,28 @@ async def test_subscribe_unsubscribe_entities_specific_entities( } +@pytest.mark.parametrize("unserializable_states", [[], ["light.cannot_serialize"]]) async def test_subscribe_unsubscribe_entities_with_filter( hass: HomeAssistant, websocket_client: MockHAClientWebSocket, hass_admin_user: MockUser, + unserializable_states: list[str], ) -> None: """Test subscribe/unsubscribe entities with an entity filter.""" + + class CannotSerializeMe: + """Cannot serialize this.""" + + def __init__(self) -> None: + """Init cannot serialize this.""" + + for entity_id in unserializable_states: + hass.states.async_set( + entity_id, + "off", + {"color": "red", "cannot_serialize": CannotSerializeMe()}, + ) + hass.states.async_set("switch.not_included", "off") hass.states.async_set("light.include", "off") await websocket_client.send_json_auto_id( From 01ff3cf9d9a3a1c0517c635ab7917cfa52483937 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Oct 2025 22:21:49 +0200 Subject: [PATCH 25/33] Start recorder data migration after schema migration (#153471) --- homeassistant/components/recorder/core.py | 6 +++-- .../components/recorder/test_v32_migration.py | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index c88a65b78c6678..b135f7a3ee8d4d 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -806,6 +806,10 @@ def _run(self) -> None: # Catch up with missed statistics self._schedule_compile_missing_statistics() + + # Kick off live migrations + migration.migrate_data_live(self, self.get_session, schema_status) + _LOGGER.debug("Recorder processing the queue") self._adjust_lru_size() self.hass.add_job(self._async_set_recorder_ready_migration_done) @@ -822,8 +826,6 @@ def _activate_and_set_db_ready( # there are a lot of statistics graphs on the frontend. self.statistics_meta_manager.load(session) - migration.migrate_data_live(self, self.get_session, schema_status) - # We must only set the db ready after we have set the table managers # to active if there is no data to migrate. # diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index 648bb03fa3e253..dd707ad6056e2a 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -845,11 +845,28 @@ def _add_data(): instrumented_migration.live_migration_done.wait ) + # The states.event_id foreign key constraint was removed when + # migration to schema version 46 + assert ( + await instance.async_add_executor_job(_get_event_id_foreign_keys) + is None + ) + + # Re-add the foreign key constraint to simulate failure to remove it during + # schema migration + with patch.object(migration, "Base", old_db_schema.Base): + await instance.async_add_executor_job( + migration._restore_foreign_key_constraints, + instance.get_session, + instance.engine, + [("states", "event_id", "events", "event_id")], + ) + # Simulate out of disk space while removing the foreign key from the states table by # - patching DropConstraint to raise InternalError for MySQL and PostgreSQL with ( patch( - "homeassistant.components.recorder.migration.sqlalchemy.inspect", + "homeassistant.components.recorder.migration.DropConstraint.__init__", side_effect=OperationalError( None, None, OSError("No space left on device") ), @@ -867,14 +884,6 @@ def _add_data(): ) states_index_names = {index["name"] for index in states_indexes} assert instance.use_legacy_events_index is True - # The states.event_id foreign key constraint was removed when - # migration to schema version 46 - assert ( - await instance.async_add_executor_job( - _get_event_id_foreign_keys - ) - is None - ) await hass.async_stop() From 7b3c96e80bdb265671a5fa4f75db185a383107a1 Mon Sep 17 00:00:00 2001 From: dollaransh17 <186504335+dollaransh17@users.noreply.github.com> Date: Fri, 3 Oct 2025 02:07:08 +0530 Subject: [PATCH 26/33] Remove deprication code for reolink Hub switches (#153483) Thank you, good work! --- homeassistant/components/reolink/strings.json | 4 - homeassistant/components/reolink/switch.py | 80 ---------- tests/components/reolink/test_switch.py | 142 +----------------- 3 files changed, 1 insertion(+), 225 deletions(-) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index d9bcc80406f3e7..dda68c6b4ad407 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -132,10 +132,6 @@ "title": "Reolink firmware update required", "description": "\"{name}\" with model \"{model}\" and hardware version \"{hw_version}\" is running an old firmware version \"{current_firmware}\", while at least firmware version \"{required_firmware}\" is required for proper operation of the Reolink integration. The firmware can be updated by pressing \"install\" in the more info dialog of the update entity of \"{name}\" from within Home Assistant. Alternatively, the latest firmware can be downloaded from the [Reolink download center]({download_link})." }, - "hub_switch_deprecated": { - "title": "Reolink Home Hub switches deprecated", - "description": "The redundant 'Record', 'Email on event', 'FTP upload', 'Push notifications', and 'Buzzer on event' switches on the Reolink Home Hub are deprecated since the new firmware no longer supports these. Please use the equally named switches under each of the camera devices connected to the Home Hub instead. To remove this issue, please adjust automations accordingly and disable the switch entities mentioned." - }, "password_too_long": { "title": "Reolink password too long", "description": "The password for \"{name}\" is more than 31 characters long, this is no longer compatible with the Reolink API. Please change the password using the Reolink app/client to a password with is shorter than 32 characters. After changing the password, fill in the new password in the Reolink Re-authentication flow to continue using this integration. The latest version of the Reolink app/client also has a password limit of 31 characters." diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index d5f45872661ea3..8431d7afb2a3ee 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -11,10 +11,8 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import ( ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription, @@ -306,56 +304,6 @@ class ReolinkChimeSwitchEntityDescription( ), ) -# Can be removed in HA 2025.4.0 -DEPRECATED_NVR_SWITCHES = [ - ReolinkNVRSwitchEntityDescription( - key="email", - cmd_key="GetEmail", - translation_key="email", - entity_category=EntityCategory.CONFIG, - supported=lambda api: api.is_hub, - value=lambda api: api.email_enabled(), - method=lambda api, value: api.set_email(None, value), - ), - ReolinkNVRSwitchEntityDescription( - key="ftp_upload", - cmd_key="GetFtp", - translation_key="ftp_upload", - entity_category=EntityCategory.CONFIG, - supported=lambda api: api.is_hub, - value=lambda api: api.ftp_enabled(), - method=lambda api, value: api.set_ftp(None, value), - ), - ReolinkNVRSwitchEntityDescription( - key="push_notifications", - cmd_key="GetPush", - translation_key="push_notifications", - entity_category=EntityCategory.CONFIG, - supported=lambda api: api.is_hub, - value=lambda api: api.push_enabled(), - method=lambda api, value: api.set_push(None, value), - ), - ReolinkNVRSwitchEntityDescription( - key="record", - cmd_key="GetRec", - translation_key="record", - entity_category=EntityCategory.CONFIG, - supported=lambda api: api.is_hub, - value=lambda api: api.recording_enabled(), - method=lambda api, value: api.set_recording(None, value), - ), - ReolinkNVRSwitchEntityDescription( - key="buzzer", - cmd_key="GetBuzzerAlarmV20", - translation_key="hub_ringtone_on_event", - icon="mdi:room-service", - entity_category=EntityCategory.CONFIG, - supported=lambda api: api.is_hub, - value=lambda api: api.buzzer_enabled(), - method=lambda api, value: api.set_buzzer(None, value), - ), -] - async def async_setup_entry( hass: HomeAssistant, @@ -389,34 +337,6 @@ async def async_setup_entry( if chime.channel is None ) - # Can be removed in HA 2025.4.0 - depricated_dict = {} - for desc in DEPRECATED_NVR_SWITCHES: - if not desc.supported(reolink_data.host.api): - continue - depricated_dict[f"{reolink_data.host.unique_id}_{desc.key}"] = desc - - entity_reg = er.async_get(hass) - reg_entities = er.async_entries_for_config_entry(entity_reg, config_entry.entry_id) - for entity in reg_entities: - # Can be removed in HA 2025.4.0 - if entity.domain == "switch" and entity.unique_id in depricated_dict: - if entity.disabled: - entity_reg.async_remove(entity.entity_id) - continue - - ir.async_create_issue( - hass, - DOMAIN, - "hub_switch_deprecated", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="hub_switch_deprecated", - ) - entities.append( - ReolinkNVRSwitchEntity(reolink_data, depricated_dict[entity.unique_id]) - ) - async_add_entities(entities) diff --git a/tests/components/reolink/test_switch.py b/tests/components/reolink/test_switch.py index 97dfc622aed043..83840cace97762 100644 --- a/tests/components/reolink/test_switch.py +++ b/tests/components/reolink/test_switch.py @@ -8,7 +8,6 @@ from reolink_aio.exceptions import ReolinkError from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL -from homeassistant.components.reolink.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -22,9 +21,8 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er, issue_registry as ir -from .conftest import TEST_CAM_NAME, TEST_NVR_NAME, TEST_UID +from .conftest import TEST_CAM_NAME, TEST_NVR_NAME from tests.common import MockConfigEntry, async_fire_time_changed @@ -228,141 +226,3 @@ async def test_chime_switch( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - - -@pytest.mark.parametrize( - ( - "original_id", - "capability", - ), - [ - ( - f"{TEST_UID}_record", - "recording", - ), - ( - f"{TEST_UID}_ftp_upload", - "ftp", - ), - ( - f"{TEST_UID}_push_notifications", - "push", - ), - ( - f"{TEST_UID}_email", - "email", - ), - ( - f"{TEST_UID}_buzzer", - "buzzer", - ), - ], -) -async def test_cleanup_hub_switches( - hass: HomeAssistant, - config_entry: MockConfigEntry, - reolink_host: MagicMock, - entity_registry: er.EntityRegistry, - original_id: str, - capability: str, -) -> None: - """Test entity ids that need to be migrated.""" - - def mock_supported(ch, cap): - if cap == capability: - return False - return True - - domain = Platform.SWITCH - - reolink_host.channels = [0] - reolink_host.is_hub = True - reolink_host.supported = mock_supported - - entity_registry.async_get_or_create( - domain=domain, - platform=DOMAIN, - unique_id=original_id, - config_entry=config_entry, - suggested_object_id=original_id, - disabled_by=er.RegistryEntryDisabler.USER, - ) - - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) - - # setup CH 0 and host entities/device - with patch("homeassistant.components.reolink.PLATFORMS", [domain]): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) is None - - -@pytest.mark.parametrize( - ( - "original_id", - "capability", - ), - [ - ( - f"{TEST_UID}_record", - "recording", - ), - ( - f"{TEST_UID}_ftp_upload", - "ftp", - ), - ( - f"{TEST_UID}_push_notifications", - "push", - ), - ( - f"{TEST_UID}_email", - "email", - ), - ( - f"{TEST_UID}_buzzer", - "buzzer", - ), - ], -) -async def test_hub_switches_repair_issue( - hass: HomeAssistant, - config_entry: MockConfigEntry, - reolink_host: MagicMock, - entity_registry: er.EntityRegistry, - issue_registry: ir.IssueRegistry, - original_id: str, - capability: str, -) -> None: - """Test entity ids that need to be migrated.""" - - def mock_supported(ch, cap): - if cap == capability: - return False - return True - - domain = Platform.SWITCH - - reolink_host.channels = [0] - reolink_host.is_hub = True - reolink_host.supported = mock_supported - - entity_registry.async_get_or_create( - domain=domain, - platform=DOMAIN, - unique_id=original_id, - config_entry=config_entry, - suggested_object_id=original_id, - disabled_by=None, - ) - - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) - - # setup CH 0 and host entities/device - with patch("homeassistant.components.reolink.PLATFORMS", [domain]): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) - assert (DOMAIN, "hub_switch_deprecated") in issue_registry.issues From e19bfd670b146460742b0ead53c50eadba8f2759 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Oct 2025 22:47:01 +0200 Subject: [PATCH 27/33] Bump recorder live schema migration to schema version 48 (#153404) --- .../components/recorder/migration.py | 8 +- tests/components/recorder/db_schema_48.py | 978 ++++++++++++++++++ tests/components/recorder/test_migrate.py | 9 +- 3 files changed, 987 insertions(+), 8 deletions(-) create mode 100644 tests/components/recorder/db_schema_48.py diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 58af15c2aa72aa..1c53b52814155c 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -117,10 +117,10 @@ if TYPE_CHECKING: from . import Recorder -# Live schema migration supported starting from schema version 42 or newer -# Schema version 41 was introduced in HA Core 2023.4 -# Schema version 42 was introduced in HA Core 2023.11 -LIVE_MIGRATION_MIN_SCHEMA_VERSION = 42 +# Live schema migration supported starting from schema version 48 or newer +# Schema version 47 was introduced in HA Core 2024.9 +# Schema version 48 was introduced in HA Core 2025.1 +LIVE_MIGRATION_MIN_SCHEMA_VERSION = 48 MIGRATION_NOTE_OFFLINE = ( "Note: this may take several hours on large databases and slow machines. " diff --git a/tests/components/recorder/db_schema_48.py b/tests/components/recorder/db_schema_48.py new file mode 100644 index 00000000000000..43587bd966db90 --- /dev/null +++ b/tests/components/recorder/db_schema_48.py @@ -0,0 +1,978 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 48. +It is used to test the schema migration logic. +""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +import time +from typing import Any, Final, Self, cast + +import ciso8601 +from fnv_hash_fast import fnv1a_32 +from sqlalchemy import ( + CHAR, + JSON, + BigInteger, + Boolean, + ColumnElement, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + LargeBinary, + SmallInteger, + String, + Text, + case, + type_coerce, +) +from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite +from sqlalchemy.engine.interfaces import Dialect +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.orm import DeclarativeBase, Mapped, aliased, mapped_column, relationship +from sqlalchemy.types import TypeDecorator + +from homeassistant.components.recorder.const import ( + ALL_DOMAIN_EXCLUDE_ATTRS, + SupportedDialect, +) +from homeassistant.components.recorder.models import ( + StatisticData, + StatisticDataTimestamp, + StatisticMetaData, + bytes_to_ulid_or_none, + bytes_to_uuid_hex_or_none, + datetime_to_timestamp_or_none, + process_timestamp, + ulid_to_bytes_or_none, + uuid_hex_to_bytes_or_none, +) +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + MATCH_ALL, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, EventStateChangedData, State +from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null +from homeassistant.util import dt as dt_util +from homeassistant.util.json import ( + JSON_DECODE_EXCEPTIONS, + json_loads, + json_loads_object, +) + + +# SQLAlchemy Schema +class Base(DeclarativeBase): + """Base class for tables.""" + + +class LegacyBase(DeclarativeBase): + """Base class for tables, used for schema migration.""" + + +SCHEMA_VERSION = 48 + +_LOGGER = logging.getLogger(__name__) + +TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" +TABLE_EVENT_TYPES = "event_types" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_STATES_META = "states_meta" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" +TABLE_MIGRATION_CHANGES = "migration_changes" + +STATISTICS_TABLES = ("statistics", "statistics_short_term") + +MAX_STATE_ATTRS_BYTES = 16384 +MAX_EVENT_DATA_BYTES = 32768 + +PSQL_DIALECT = SupportedDialect.POSTGRESQL + +ALL_TABLES = [ + TABLE_STATES, + TABLE_STATE_ATTRIBUTES, + TABLE_EVENTS, + TABLE_EVENT_DATA, + TABLE_EVENT_TYPES, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_MIGRATION_CHANGES, + TABLE_STATES_META, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + +LAST_UPDATED_INDEX_TS = "ix_states_last_updated_ts" +METADATA_ID_LAST_UPDATED_INDEX_TS = "ix_states_metadata_id_last_updated_ts" +EVENTS_CONTEXT_ID_BIN_INDEX = "ix_events_context_id_bin" +STATES_CONTEXT_ID_BIN_INDEX = "ix_states_context_id_bin" +LEGACY_STATES_EVENT_ID_INDEX = "ix_states_event_id" +LEGACY_STATES_ENTITY_ID_LAST_UPDATED_TS_INDEX = "ix_states_entity_id_last_updated_ts" +LEGACY_MAX_LENGTH_EVENT_CONTEXT_ID: Final = 36 +CONTEXT_ID_BIN_MAX_LENGTH = 16 + +MYSQL_COLLATE = "utf8mb4_unicode_ci" +MYSQL_DEFAULT_CHARSET = "utf8mb4" +MYSQL_ENGINE = "InnoDB" + +_DEFAULT_TABLE_ARGS = { + "mysql_default_charset": MYSQL_DEFAULT_CHARSET, + "mysql_collate": MYSQL_COLLATE, + "mysql_engine": MYSQL_ENGINE, + "mariadb_default_charset": MYSQL_DEFAULT_CHARSET, + "mariadb_collate": MYSQL_COLLATE, + "mariadb_engine": MYSQL_ENGINE, +} + +_MATCH_ALL_KEEP = { + ATTR_DEVICE_CLASS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_FRIENDLY_NAME, +} + + +class UnusedDateTime(DateTime): + """An unused column type that behaves like a datetime.""" + + +class Unused(CHAR): + """An unused column type that behaves like a string.""" + + +@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") +@compiles(Unused, "mysql", "mariadb", "sqlite") +def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: + """Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite.""" + return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite) + + +@compiles(Unused, "postgresql") +def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: + """Compile Unused as CHAR(1) on postgresql.""" + return "CHAR(1)" # Uses 1 byte + + +class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): + """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" + + def result_processor(self, dialect: Dialect, coltype: Any) -> Callable | None: + """Offload the datetime parsing to ciso8601.""" + return lambda value: None if value is None else ciso8601.parse_datetime(value) + + +class NativeLargeBinary(LargeBinary): + """A faster version of LargeBinary for engines that support python bytes natively.""" + + def result_processor(self, dialect: Dialect, coltype: Any) -> Callable | None: + """No conversion needed for engines that support native bytes.""" + return None + + +# Although all integers are same in SQLite, it does not allow an identity column to be BIGINT +# https://sqlite.org/forum/info/2dfa968a702e1506e885cb06d92157d492108b22bf39459506ab9f7125bca7fd +ID_TYPE = BigInteger().with_variant(sqlite.INTEGER, "sqlite") +# For MariaDB and MySQL we can use an unsigned integer type since it will fit 2**32 +# for sqlite and postgresql we use a bigint +UINT_32_TYPE = BigInteger().with_variant( + mysql.INTEGER(unsigned=True), # type: ignore[no-untyped-call] + "mysql", + "mariadb", +) +JSON_VARIANT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", +) +JSONB_VARIANT_CAST = Text().with_variant( + postgresql.JSONB(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", +) +DATETIME_TYPE = ( + DateTime(timezone=True) + .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql", "mariadb") # type: ignore[no-untyped-call] + .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") # type: ignore[no-untyped-call] +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql", "mariadb") # type: ignore[no-untyped-call] + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) +UNUSED_LEGACY_COLUMN = Unused(0) +UNUSED_LEGACY_DATETIME_COLUMN = UnusedDateTime(timezone=True) +UNUSED_LEGACY_INTEGER_COLUMN = SmallInteger() +DOUBLE_PRECISION_TYPE_SQL = "DOUBLE PRECISION" +BIG_INTEGER_SQL = "BIGINT" +CONTEXT_BINARY_TYPE = LargeBinary(CONTEXT_ID_BIN_MAX_LENGTH).with_variant( + NativeLargeBinary(CONTEXT_ID_BIN_MAX_LENGTH), "mysql", "mariadb", "sqlite" +) + +TIMESTAMP_TYPE = DOUBLE_TYPE + + +class JSONLiteral(JSON): + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: Dialect) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return JSON_DUMP(value) + + return process + + +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] + + +class Events(Base): + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index( + "ix_events_event_type_id_time_fired_ts", "event_type_id", "time_fired_ts" + ), + Index( + EVENTS_CONTEXT_ID_BIN_INDEX, + "context_id_bin", + mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, + mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_EVENTS + event_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + event_type: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + event_data: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin_idx: Mapped[int | None] = mapped_column(SmallInteger) + time_fired: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + time_fired_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) + context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + data_id: Mapped[int | None] = mapped_column( + ID_TYPE, ForeignKey("event_data.data_id"), index=True + ) + context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + event_type_id: Mapped[int | None] = mapped_column( + ID_TYPE, ForeignKey("event_types.event_type_id") + ) + event_data_rel: Mapped[EventData | None] = relationship("EventData") + event_type_rel: Mapped[EventTypes | None] = relationship("EventTypes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @property + def _time_fired_isotime(self) -> str | None: + """Return time_fired as an isotime string.""" + date_time: datetime | None + if self.time_fired_ts is not None: + date_time = dt_util.utc_from_timestamp(self.time_fired_ts) + else: + date_time = process_timestamp(self.time_fired) + if date_time is None: + return None + return date_time.isoformat(sep=" ", timespec="seconds") + + @staticmethod + def from_event(event: Event) -> Events: + """Create an event database object from a native event.""" + context = event.context + return Events( + event_type=None, + event_data=None, + origin_idx=event.origin.idx, + time_fired=None, + time_fired_ts=event.time_fired_timestamp, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=bytes_to_ulid_or_none(self.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), + ) + try: + return Event( + self.event_type or "", + json_loads_object(self.event_data) if self.event_data else {}, + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx or 0], + self.time_fired_ts or 0, + context=context, + ) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class LegacyEvents(LegacyBase): + """Event history data with event_id, used for schema migration.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENTS + event_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + context_id: Mapped[str | None] = mapped_column( + String(LEGACY_MAX_LENGTH_EVENT_CONTEXT_ID), index=True + ) + + +class EventData(Base): + """Event data history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENT_DATA + data_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data: Mapped[str | None] = mapped_column( + Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @staticmethod + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: + """Create shared_data from an event.""" + encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes + bytes_result = encoder(event.data) + if len(bytes_result) > MAX_EVENT_DATA_BYTES: + _LOGGER.warning( + "Event data for %s exceed maximum size of %s bytes. " + "This can cause database performance issues; Event data " + "will not be stored", + event.event_type, + MAX_EVENT_DATA_BYTES, + ) + return b"{}" + return bytes_result + + @staticmethod + def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: + """Return the hash of json encoded shared data.""" + return fnv1a_32(shared_data_bytes) + + def to_native(self) -> dict[str, Any]: + """Convert to an event data dictionary.""" + shared_data = self.shared_data + if shared_data is None: + return {} + try: + return cast(dict[str, Any], json_loads(shared_data)) + except JSON_DECODE_EXCEPTIONS: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + +class EventTypes(Base): + """Event type history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENT_TYPES + event_type_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + event_type: Mapped[str | None] = mapped_column( + String(MAX_LENGTH_EVENT_EVENT_TYPE), index=True, unique=True + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class States(Base): + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index(METADATA_ID_LAST_UPDATED_INDEX_TS, "metadata_id", "last_updated_ts"), + Index( + STATES_CONTEXT_ID_BIN_INDEX, + "context_id_bin", + mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, + mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_STATES + state_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + state: Mapped[str | None] = mapped_column(String(MAX_LENGTH_STATE_STATE)) + attributes: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + event_id: Mapped[int | None] = mapped_column(UNUSED_LEGACY_INTEGER_COLUMN) + last_changed: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_changed_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + last_reported_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + last_updated: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_updated_ts: Mapped[float | None] = mapped_column( + TIMESTAMP_TYPE, default=time.time, index=True + ) + old_state_id: Mapped[int | None] = mapped_column( + ID_TYPE, ForeignKey("states.state_id"), index=True + ) + attributes_id: Mapped[int | None] = mapped_column( + ID_TYPE, ForeignKey("state_attributes.attributes_id"), index=True + ) + context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin_idx: Mapped[int | None] = mapped_column( + SmallInteger + ) # 0 is local, 1 is remote + old_state: Mapped[States | None] = relationship("States", remote_side=[state_id]) + state_attributes: Mapped[StateAttributes | None] = relationship("StateAttributes") + context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + metadata_id: Mapped[int | None] = mapped_column( + ID_TYPE, ForeignKey("states_meta.metadata_id") + ) + states_meta_rel: Mapped[StatesMeta | None] = relationship("StatesMeta") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @property + def _last_updated_isotime(self) -> str | None: + """Return last_updated as an isotime string.""" + date_time: datetime | None + if self.last_updated_ts is not None: + date_time = dt_util.utc_from_timestamp(self.last_updated_ts) + else: + date_time = process_timestamp(self.last_updated) + if date_time is None: + return None + return date_time.isoformat(sep=" ", timespec="seconds") + + @staticmethod + def from_event(event: Event[EventStateChangedData]) -> States: + """Create object from a state_changed event.""" + state = event.data["new_state"] + # None state means the state was removed from the state machine + if state is None: + state_value = "" + last_updated_ts = event.time_fired_timestamp + last_changed_ts = None + last_reported_ts = None + else: + state_value = state.state + last_updated_ts = state.last_updated_timestamp + if state.last_updated == state.last_changed: + last_changed_ts = None + else: + last_changed_ts = state.last_changed_timestamp + if state.last_updated == state.last_reported: + last_reported_ts = None + else: + last_reported_ts = state.last_reported_timestamp + context = event.context + return States( + state=state_value, + entity_id=event.data["entity_id"], + attributes=None, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), + origin_idx=event.origin.idx, + last_updated=None, + last_changed=None, + last_updated_ts=last_updated_ts, + last_changed_ts=last_changed_ts, + last_reported_ts=last_reported_ts, + ) + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = Context( + id=bytes_to_ulid_or_none(self.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), + ) + try: + attrs = json_loads_object(self.attributes) if self.attributes else {} + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: + last_changed = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + else: + last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) + if ( + self.last_reported_ts is None + or self.last_reported_ts == self.last_updated_ts + ): + last_reported = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + else: + last_reported = dt_util.utc_from_timestamp(self.last_reported_ts or 0) + return State( + self.entity_id or "", + self.state, # type: ignore[arg-type] + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed=last_changed, + last_reported=last_reported, + last_updated=last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) + + +class LegacyStates(LegacyBase): + """State change history with entity_id, used for schema migration.""" + + __table_args__ = ( + Index( + LEGACY_STATES_ENTITY_ID_LAST_UPDATED_TS_INDEX, + "entity_id", + "last_updated_ts", + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_STATES + state_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + last_updated_ts: Mapped[float | None] = mapped_column( + TIMESTAMP_TYPE, default=time.time, index=True + ) + context_id: Mapped[str | None] = mapped_column( + String(LEGACY_MAX_LENGTH_EVENT_CONTEXT_ID), index=True + ) + + +class StateAttributes(Base): + """State attribute change history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs: Mapped[str | None] = mapped_column( + Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def shared_attrs_bytes_from_event( + event: Event[EventStateChangedData], + dialect: SupportedDialect | None, + ) -> bytes: + """Create shared_attrs from a state_changed event.""" + # None state means the state was removed from the state machine + if (state := event.data["new_state"]) is None: + return b"{}" + if state_info := state.state_info: + unrecorded_attributes = state_info["unrecorded_attributes"] + exclude_attrs = { + *ALL_DOMAIN_EXCLUDE_ATTRS, + *unrecorded_attributes, + } + if MATCH_ALL in unrecorded_attributes: + # Don't exclude device class, state class, unit of measurement + # or friendly name when using the MATCH_ALL exclude constant + exclude_attrs.update(state.attributes) + exclude_attrs -= _MATCH_ALL_KEEP + else: + exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS + encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes + bytes_result = encoder( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + if len(bytes_result) > MAX_STATE_ATTRS_BYTES: + _LOGGER.warning( + "State attributes for %s exceed maximum size of %s bytes. " + "This can cause database performance issues; Attributes " + "will not be stored", + state.entity_id, + MAX_STATE_ATTRS_BYTES, + ) + return b"{}" + return bytes_result + + @staticmethod + def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: + """Return the hash of json encoded shared attributes.""" + return fnv1a_32(shared_attrs_bytes) + + def to_native(self) -> dict[str, Any]: + """Convert to a state attributes dictionary.""" + shared_attrs = self.shared_attrs + if shared_attrs is None: + return {} + try: + return cast(dict[str, Any], json_loads(shared_attrs)) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatesMeta(Base): + """Metadata for states.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATES_META + metadata_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column( + String(MAX_LENGTH_STATE_ENTITY_ID), index=True, unique=True + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsBase: + """Statistics base class.""" + + id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + created: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + created_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, default=time.time) + metadata_id: Mapped[int | None] = mapped_column( + ID_TYPE, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + ) + start: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + start_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) + mean: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + min: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + max: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + last_reset: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_reset_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + state: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + sum: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + + duration: timedelta + + @classmethod + def from_stats( + cls, metadata_id: int, stats: StatisticData, now_timestamp: float | None = None + ) -> Self: + """Create object from a statistics with datetime objects.""" + return cls( # type: ignore[call-arg] + metadata_id=metadata_id, + created=None, + created_ts=now_timestamp or time.time(), + start=None, + start_ts=stats["start"].timestamp(), + mean=stats.get("mean"), + min=stats.get("min"), + max=stats.get("max"), + last_reset=None, + last_reset_ts=datetime_to_timestamp_or_none(stats.get("last_reset")), + state=stats.get("state"), + sum=stats.get("sum"), + ) + + @classmethod + def from_stats_ts( + cls, + metadata_id: int, + stats: StatisticDataTimestamp, + now_timestamp: float | None = None, + ) -> Self: + """Create object from a statistics with timestamps.""" + return cls( # type: ignore[call-arg] + metadata_id=metadata_id, + created=None, + created_ts=now_timestamp or time.time(), + start=None, + start_ts=stats["start_ts"], + mean=stats.get("mean"), + min=stats.get("min"), + max=stats.get("max"), + last_reset=None, + last_reset_ts=stats.get("last_reset_ts"), + state=stats.get("state"), + sum=stats.get("sum"), + ) + + +class Statistics(Base, StatisticsBase): + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_statistic_id_start_ts", + "metadata_id", + "start_ts", + unique=True, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_STATISTICS + + +class _StatisticsShortTerm(StatisticsBase): + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticsShortTerm(Base, _StatisticsShortTerm): + """Short term statistics.""" + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start_ts", + "metadata_id", + "start_ts", + unique=True, + ), + _DEFAULT_TABLE_ARGS, + ) + + +class LegacyStatisticsShortTerm(LegacyBase, _StatisticsShortTerm): + """Short term statistics with 32-bit index, used for schema migration.""" + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start_ts", + "metadata_id", + "start_ts", + unique=True, + ), + _DEFAULT_TABLE_ARGS, + ) + + metadata_id: Mapped[int | None] = mapped_column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + use_existing_column=True, + ) + + +class _StatisticsMeta: + """Statistics meta data.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATISTICS_META + id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + statistic_id: Mapped[str | None] = mapped_column( + String(255), index=True, unique=True + ) + source: Mapped[str | None] = mapped_column(String(32)) + unit_of_measurement: Mapped[str | None] = mapped_column(String(255)) + has_mean: Mapped[bool | None] = mapped_column(Boolean) + has_sum: Mapped[bool | None] = mapped_column(Boolean) + name: Mapped[str | None] = mapped_column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class StatisticsMeta(Base, _StatisticsMeta): + """Statistics meta data.""" + + +class LegacyStatisticsMeta(LegacyBase, _StatisticsMeta): + """Statistics meta data with 32-bit index, used for schema migration.""" + + id: Mapped[int] = mapped_column( + Integer, + Identity(), + primary_key=True, + use_existing_column=True, + ) + + +class RecorderRuns(Base): + """Representation of recorder run.""" + + __table_args__ = ( + Index("ix_recorder_runs_start_end", "start", "end"), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_RECORDER_RUNS + run_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + end: Mapped[datetime | None] = mapped_column(DATETIME_TYPE) + closed_incorrect: Mapped[bool] = mapped_column(Boolean, default=False) + created: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def to_native(self, validate_entity_id: bool = True) -> Self: + """Return self, native format is this model.""" + return self + + +class MigrationChanges(Base): + """Representation of migration changes.""" + + __tablename__ = TABLE_MIGRATION_CHANGES + __table_args__ = (_DEFAULT_TABLE_ARGS,) + + migration_id: Mapped[str] = mapped_column(String(255), primary_key=True) + version: Mapped[int] = mapped_column(SmallInteger) + + +class SchemaChanges(Base): + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + __table_args__ = (_DEFAULT_TABLE_ARGS,) + + change_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + schema_version: Mapped[int | None] = mapped_column(Integer) + changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsRuns(Base): + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + __table_args__ = (_DEFAULT_TABLE_ARGS,) + + run_id: Mapped[int] = mapped_column(ID_TYPE, Identity(), primary_key=True) + start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: ColumnElement = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") + +SHARED_ATTR_OR_LEGACY_ATTRIBUTES = case( + (StateAttributes.shared_attrs.is_(None), States.attributes), + else_=StateAttributes.shared_attrs, +).label("attributes") +SHARED_DATA_OR_LEGACY_EVENT_DATA = case( + (EventData.shared_data.is_(None), Events.event_data), else_=EventData.shared_data +).label("event_data") diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index fe0c7454ebd1e4..74d319bcd97a41 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -107,7 +107,7 @@ async def test_schema_update_calls( schema_errors=set(), start_version=0, ), - 42, + 48, ), call( instance, @@ -115,7 +115,7 @@ async def test_schema_update_calls( engine, session_maker, migration.SchemaValidationStatus( - current_version=42, + current_version=48, initial_version=0, migration_needed=True, non_live_data_migration_needed=True, @@ -233,7 +233,7 @@ async def test_database_migration_failed( # Test error handling in _modify_columns (12, "sqlalchemy.engine.base.Connection.execute", False, 1, 0), # Test error handling in _drop_foreign_key_constraints - (46, "homeassistant.components.recorder.migration.DropConstraint", False, 2, 1), + (46, "homeassistant.components.recorder.migration.DropConstraint", False, 1, 0), ], ) @pytest.mark.skip_on_db_engine(["sqlite"]) @@ -560,7 +560,8 @@ async def test_events_during_migration_queue_exhausted( (18, False), (22, False), (25, False), - (43, True), + (43, False), + (48, True), ], ) async def test_schema_migrate( From b87910e596b71f6f42b8a08339e6f1188e79c192 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 2 Oct 2025 22:49:39 +0200 Subject: [PATCH 28/33] Bump reolink-aio to 0.16.1 (#153489) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index c547aee39c2f35..116c2928ff3465 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.16.0"] + "requirements": ["reolink-aio==0.16.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8231fabe0062e0..b9a377b524fee5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2710,7 +2710,7 @@ renault-api==0.4.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.16.0 +reolink-aio==0.16.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c680d74e1e99c..403439864ac538 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2256,7 +2256,7 @@ renault-api==0.4.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.16.0 +reolink-aio==0.16.1 # homeassistant.components.rflink rflink==0.0.67 From 71b3ebd15aeba51da1c54171f005ca4a63285c98 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 2 Oct 2025 22:49:55 +0200 Subject: [PATCH 29/33] Cleanup reolink update entity migration (#153492) --- homeassistant/components/reolink/__init__.py | 12 +----------- tests/components/reolink/test_init.py | 18 ------------------ 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 81e000d8a75567..a10a926f6e50fb 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -25,7 +25,7 @@ device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -497,16 +497,6 @@ def migrate_entity_ids( entity_reg = er.async_get(hass) entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) for entity in entities: - # Can be removed in HA 2025.1.0 - if entity.domain == "update" and entity.unique_id in [ - host.unique_id, - format_mac(host.api.mac_address), - ]: - entity_reg.async_update_entity( - entity.entity_id, new_unique_id=f"{host.unique_id}_firmware" - ) - continue - if host.api.supported(None, "UID") and not entity.unique_id.startswith( host.unique_id ): diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 662469ebc01685..00ef9e59e3b97c 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -338,24 +338,6 @@ async def test_remove_chime(*args, **key_args): "support_ch_uid", ), [ - ( - TEST_MAC, - f"{TEST_MAC}_firmware", - f"{TEST_MAC}", - f"{TEST_MAC}", - Platform.UPDATE, - False, - False, - ), - ( - TEST_MAC, - f"{TEST_UID}_firmware", - f"{TEST_MAC}", - f"{TEST_UID}", - Platform.UPDATE, - True, - False, - ), ( f"{TEST_MAC}_0_record_audio", f"{TEST_UID}_0_record_audio", From 70552766650e83079dfffdedaf943f75f0d83a16 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 2 Oct 2025 23:09:17 +0200 Subject: [PATCH 30/33] Allign naming of Reolink host switch entities (#153494) --- homeassistant/components/reolink/switch.py | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index 8431d7afb2a3ee..dfc47e7fa796ff 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -39,11 +39,11 @@ class ReolinkSwitchEntityDescription( @dataclass(frozen=True, kw_only=True) -class ReolinkNVRSwitchEntityDescription( +class ReolinkHostSwitchEntityDescription( SwitchEntityDescription, ReolinkHostEntityDescription, ): - """A class that describes NVR switch entities.""" + """A class that describes host switch entities.""" method: Callable[[Host, bool], Any] value: Callable[[Host], bool] @@ -245,8 +245,8 @@ class ReolinkChimeSwitchEntityDescription( ), ) -NVR_SWITCH_ENTITIES = ( - ReolinkNVRSwitchEntityDescription( +HOST_SWITCH_ENTITIES = ( + ReolinkHostSwitchEntityDescription( key="email", cmd_key="GetEmail", translation_key="email", @@ -255,7 +255,7 @@ class ReolinkChimeSwitchEntityDescription( value=lambda api: api.email_enabled(), method=lambda api, value: api.set_email(None, value), ), - ReolinkNVRSwitchEntityDescription( + ReolinkHostSwitchEntityDescription( key="ftp_upload", cmd_key="GetFtp", translation_key="ftp_upload", @@ -264,7 +264,7 @@ class ReolinkChimeSwitchEntityDescription( value=lambda api: api.ftp_enabled(), method=lambda api, value: api.set_ftp(None, value), ), - ReolinkNVRSwitchEntityDescription( + ReolinkHostSwitchEntityDescription( key="push_notifications", cmd_key="GetPush", translation_key="push_notifications", @@ -273,7 +273,7 @@ class ReolinkChimeSwitchEntityDescription( value=lambda api: api.push_enabled(), method=lambda api, value: api.set_push(None, value), ), - ReolinkNVRSwitchEntityDescription( + ReolinkHostSwitchEntityDescription( key="record", cmd_key="GetRec", translation_key="record", @@ -282,7 +282,7 @@ class ReolinkChimeSwitchEntityDescription( value=lambda api: api.recording_enabled(), method=lambda api, value: api.set_recording(None, value), ), - ReolinkNVRSwitchEntityDescription( + ReolinkHostSwitchEntityDescription( key="buzzer", cmd_key="GetBuzzerAlarmV20", translation_key="hub_ringtone_on_event", @@ -320,8 +320,8 @@ async def async_setup_entry( if entity_description.supported(reolink_data.host.api, channel) ] entities.extend( - ReolinkNVRSwitchEntity(reolink_data, entity_description) - for entity_description in NVR_SWITCH_ENTITIES + ReolinkHostSwitchEntity(reolink_data, entity_description) + for entity_description in HOST_SWITCH_ENTITIES if entity_description.supported(reolink_data.host.api) ) entities.extend( @@ -373,15 +373,15 @@ async def async_turn_off(self, **kwargs: Any) -> None: self.async_write_ha_state() -class ReolinkNVRSwitchEntity(ReolinkHostCoordinatorEntity, SwitchEntity): - """Switch entity class for Reolink NVR features.""" +class ReolinkHostSwitchEntity(ReolinkHostCoordinatorEntity, SwitchEntity): + """Switch entity class for Reolink host features.""" - entity_description: ReolinkNVRSwitchEntityDescription + entity_description: ReolinkHostSwitchEntityDescription def __init__( self, reolink_data: ReolinkData, - entity_description: ReolinkNVRSwitchEntityDescription, + entity_description: ReolinkHostSwitchEntityDescription, ) -> None: """Initialize Reolink switch entity.""" self.entity_description = entity_description From 67644636893b0ae70858dcbcb9c39fcf3b77b1ea Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 2 Oct 2025 23:09:43 +0200 Subject: [PATCH 31/33] Use new Reolink rec_enable flag (#153496) --- homeassistant/components/reolink/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index dfc47e7fa796ff..b7d249b6fecb63 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -154,7 +154,7 @@ class ReolinkChimeSwitchEntityDescription( cmd_key="GetRec", translation_key="record", entity_category=EntityCategory.CONFIG, - supported=lambda api, ch: api.supported(ch, "recording") and api.is_nvr, + supported=lambda api, ch: api.supported(ch, "rec_enable") and api.is_nvr, value=lambda api, ch: api.recording_enabled(ch), method=lambda api, ch, value: api.set_recording(ch, value), ), @@ -278,7 +278,7 @@ class ReolinkChimeSwitchEntityDescription( cmd_key="GetRec", translation_key="record", entity_category=EntityCategory.CONFIG, - supported=lambda api: api.supported(None, "recording") and not api.is_hub, + supported=lambda api: api.supported(None, "rec_enable") and not api.is_hub, value=lambda api: api.recording_enabled(), method=lambda api, value: api.set_recording(None, value), ), From 12085e61528edd0fa707b68cdb331eaf2c8fa183 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 2 Oct 2025 23:10:27 +0200 Subject: [PATCH 32/33] Improve Reolink docstrings (#153498) --- homeassistant/components/reolink/entity.py | 2 +- homeassistant/components/reolink/sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index dcda6b843ad15f..c180e5f77b2601 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -252,7 +252,7 @@ def __init__( chime: Chime, coordinator: DataUpdateCoordinator[None] | None = None, ) -> None: - """Initialize ReolinkChimeCoordinatorEntity for a chime.""" + """Initialize ReolinkHostChimeCoordinatorEntity for a chime.""" super().__init__(reolink_data, coordinator) self._channel = chime.channel self._chime = chime diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index a0939046a1728b..fe9744543c036b 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -284,7 +284,7 @@ def native_value(self) -> StateType | date | datetime | Decimal: class ReolinkHddSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): - """Base sensor class for Reolink host sensors.""" + """Base sensor class for Reolink storage device sensors.""" entity_description: ReolinkSensorEntityDescription @@ -294,7 +294,7 @@ def __init__( hdd_index: int, entity_description: ReolinkSensorEntityDescription, ) -> None: - """Initialize Reolink host sensor.""" + """Initialize Reolink storage device sensor.""" self.entity_description = entity_description super().__init__(reolink_data) self._hdd_index = hdd_index From 78e16495bde626701cd1d18fcec7db8f6dd26c5b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Oct 2025 23:15:15 +0200 Subject: [PATCH 33/33] Remove runtime support for recorder DB without States.last_reported_ts (#153495) --- homeassistant/components/recorder/const.py | 1 - homeassistant/components/recorder/core.py | 3 +- .../components/recorder/history/modern.py | 28 +- tests/components/recorder/db_schema_42.py | 859 -------------- .../recorder/test_history_db_schema_42.py | 1022 ----------------- 5 files changed, 10 insertions(+), 1903 deletions(-) delete mode 100644 tests/components/recorder/db_schema_42.py delete mode 100644 tests/components/recorder/test_history_db_schema_42.py diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 4797eecda0fbbc..b1563d85d56656 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -53,7 +53,6 @@ CONTEXT_ID_AS_BINARY_SCHEMA_VERSION = 36 EVENT_TYPE_IDS_SCHEMA_VERSION = 37 STATES_META_SCHEMA_VERSION = 38 -LAST_REPORTED_SCHEMA_VERSION = 43 CIRCULAR_MEAN_SCHEMA_VERSION = 49 LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28 diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index b135f7a3ee8d4d..d662416012f315 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -56,7 +56,6 @@ DEFAULT_MAX_BIND_VARS, DOMAIN, KEEPALIVE_TIME, - LAST_REPORTED_SCHEMA_VERSION, MARIADB_PYMYSQL_URL_PREFIX, MARIADB_URL_PREFIX, MAX_QUEUE_BACKLOG_MIN_VALUE, @@ -1226,7 +1225,7 @@ def _commit_event_session(self) -> None: if ( pending_last_reported := self.states_manager.get_pending_last_reported_timestamp() - ) and self.schema_version >= LAST_REPORTED_SCHEMA_VERSION: + ): with session.no_autoflush: session.execute( update(States), diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index 566e30713f0650..a636ed34ef4d8b 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -29,7 +29,7 @@ from homeassistant.util import dt as dt_util from homeassistant.util.collection import chunked_or_all -from ..const import LAST_REPORTED_SCHEMA_VERSION, MAX_IDS_FOR_INDEXED_GROUP_BY +from ..const import MAX_IDS_FOR_INDEXED_GROUP_BY from ..db_schema import ( SHARED_ATTR_OR_LEGACY_ATTRIBUTES, StateAttributes, @@ -388,10 +388,9 @@ def _state_changed_during_period_stmt( limit: int | None, include_start_time_state: bool, run_start_ts: float | None, - include_last_reported: bool, ) -> Select | CompoundSelect: stmt = ( - _stmt_and_join_attributes(no_attributes, False, include_last_reported) + _stmt_and_join_attributes(no_attributes, False, True) .filter( ( (States.last_changed_ts == States.last_updated_ts) @@ -424,22 +423,22 @@ def _state_changed_during_period_stmt( single_metadata_id, no_attributes, False, - include_last_reported, + True, ).subquery(), no_attributes, False, - include_last_reported, + True, ), _select_from_subquery( stmt.subquery(), no_attributes, False, - include_last_reported, + True, ), ).subquery(), no_attributes, False, - include_last_reported, + True, ) @@ -454,9 +453,6 @@ def state_changes_during_period( include_start_time_state: bool = True, ) -> dict[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" - has_last_reported = ( - get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION - ) if not entity_id: raise ValueError("entity_id must be provided") entity_ids = [entity_id.lower()] @@ -489,14 +485,12 @@ def state_changes_during_period( limit, include_start_time_state, oldest_ts, - has_last_reported, ), track_on=[ bool(end_time_ts), no_attributes, bool(limit), include_start_time_state, - has_last_reported, ], ) return cast( @@ -543,10 +537,10 @@ def _get_last_state_changes_single_stmt(metadata_id: int) -> Select: def _get_last_state_changes_multiple_stmt( - number_of_states: int, metadata_id: int, include_last_reported: bool + number_of_states: int, metadata_id: int ) -> Select: return ( - _stmt_and_join_attributes(False, False, include_last_reported) + _stmt_and_join_attributes(False, False, True) .where( States.state_id == ( @@ -568,9 +562,6 @@ def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str ) -> dict[str, list[State]]: """Return the last number_of_states.""" - has_last_reported = ( - get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION - ) entity_id_lower = entity_id.lower() entity_ids = [entity_id_lower] @@ -595,9 +586,8 @@ def get_last_state_changes( else: stmt = lambda_stmt( lambda: _get_last_state_changes_multiple_stmt( - number_of_states, metadata_id, has_last_reported + number_of_states, metadata_id ), - track_on=[has_last_reported], ) states = list(execute_stmt_lambda_element(session, stmt, orm_rows=False)) return cast( diff --git a/tests/components/recorder/db_schema_42.py b/tests/components/recorder/db_schema_42.py deleted file mode 100644 index b0cdecd88dc955..00000000000000 --- a/tests/components/recorder/db_schema_42.py +++ /dev/null @@ -1,859 +0,0 @@ -"""Models for SQLAlchemy. - -This file contains the model definitions for schema version 42. -It is used to test the schema migration logic. -""" - -from __future__ import annotations - -from collections.abc import Callable -from datetime import datetime, timedelta -import logging -import time -from typing import Any, Self, cast - -import ciso8601 -from fnv_hash_fast import fnv1a_32 -from sqlalchemy import ( - CHAR, - JSON, - BigInteger, - Boolean, - ColumnElement, - DateTime, - Float, - ForeignKey, - Identity, - Index, - Integer, - LargeBinary, - SmallInteger, - String, - Text, - case, - type_coerce, -) -from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite -from sqlalchemy.engine.interfaces import Dialect -from sqlalchemy.ext.compiler import compiles -from sqlalchemy.orm import DeclarativeBase, Mapped, aliased, mapped_column, relationship -from sqlalchemy.types import TypeDecorator - -from homeassistant.components.recorder.const import ( - ALL_DOMAIN_EXCLUDE_ATTRS, - SupportedDialect, -) -from homeassistant.components.recorder.models import ( - StatisticData, - StatisticDataTimestamp, - StatisticMetaData, - bytes_to_ulid_or_none, - bytes_to_uuid_hex_or_none, - datetime_to_timestamp_or_none, - process_timestamp, - ulid_to_bytes_or_none, - uuid_hex_to_bytes_or_none, -) -from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_FRIENDLY_NAME, - ATTR_UNIT_OF_MEASUREMENT, - MATCH_ALL, - MAX_LENGTH_EVENT_EVENT_TYPE, - MAX_LENGTH_STATE_ENTITY_ID, - MAX_LENGTH_STATE_STATE, -) -from homeassistant.core import Context, Event, EventOrigin, State -from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null -from homeassistant.util import dt as dt_util -from homeassistant.util.json import ( - JSON_DECODE_EXCEPTIONS, - json_loads, - json_loads_object, -) - - -# SQLAlchemy Schema -class Base(DeclarativeBase): - """Base class for tables.""" - - -SCHEMA_VERSION = 42 - -_LOGGER = logging.getLogger(__name__) - -TABLE_EVENTS = "events" -TABLE_EVENT_DATA = "event_data" -TABLE_EVENT_TYPES = "event_types" -TABLE_STATES = "states" -TABLE_STATE_ATTRIBUTES = "state_attributes" -TABLE_STATES_META = "states_meta" -TABLE_RECORDER_RUNS = "recorder_runs" -TABLE_SCHEMA_CHANGES = "schema_changes" -TABLE_STATISTICS = "statistics" -TABLE_STATISTICS_META = "statistics_meta" -TABLE_STATISTICS_RUNS = "statistics_runs" -TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" - -STATISTICS_TABLES = ("statistics", "statistics_short_term") - -MAX_STATE_ATTRS_BYTES = 16384 -MAX_EVENT_DATA_BYTES = 32768 - -PSQL_DIALECT = SupportedDialect.POSTGRESQL - -ALL_TABLES = [ - TABLE_STATES, - TABLE_STATE_ATTRIBUTES, - TABLE_EVENTS, - TABLE_EVENT_DATA, - TABLE_EVENT_TYPES, - TABLE_RECORDER_RUNS, - TABLE_SCHEMA_CHANGES, - TABLE_STATES_META, - TABLE_STATISTICS, - TABLE_STATISTICS_META, - TABLE_STATISTICS_RUNS, - TABLE_STATISTICS_SHORT_TERM, -] - -TABLES_TO_CHECK = [ - TABLE_STATES, - TABLE_EVENTS, - TABLE_RECORDER_RUNS, - TABLE_SCHEMA_CHANGES, -] - -LAST_UPDATED_INDEX_TS = "ix_states_last_updated_ts" -METADATA_ID_LAST_UPDATED_INDEX_TS = "ix_states_metadata_id_last_updated_ts" -EVENTS_CONTEXT_ID_BIN_INDEX = "ix_events_context_id_bin" -STATES_CONTEXT_ID_BIN_INDEX = "ix_states_context_id_bin" -LEGACY_STATES_EVENT_ID_INDEX = "ix_states_event_id" -LEGACY_STATES_ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated_ts" -CONTEXT_ID_BIN_MAX_LENGTH = 16 - -MYSQL_COLLATE = "utf8mb4_unicode_ci" -MYSQL_DEFAULT_CHARSET = "utf8mb4" -MYSQL_ENGINE = "InnoDB" - -_DEFAULT_TABLE_ARGS = { - "mysql_default_charset": MYSQL_DEFAULT_CHARSET, - "mysql_collate": MYSQL_COLLATE, - "mysql_engine": MYSQL_ENGINE, - "mariadb_default_charset": MYSQL_DEFAULT_CHARSET, - "mariadb_collate": MYSQL_COLLATE, - "mariadb_engine": MYSQL_ENGINE, -} - - -class UnusedDateTime(DateTime): - """An unused column type that behaves like a datetime.""" - - -class Unused(CHAR): - """An unused column type that behaves like a string.""" - - -@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] -@compiles(Unused, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] -def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: - """Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite.""" - return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite) - - -@compiles(Unused, "postgresql") # type: ignore[misc,no-untyped-call] -def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: - """Compile Unused as CHAR(1) on postgresql.""" - return "CHAR(1)" # Uses 1 byte - - -class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): - """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" - - def result_processor(self, dialect: Dialect, coltype: Any) -> Callable | None: - """Offload the datetime parsing to ciso8601.""" - return lambda value: None if value is None else ciso8601.parse_datetime(value) - - -class NativeLargeBinary(LargeBinary): - """A faster version of LargeBinary for engines that support python bytes natively.""" - - def result_processor(self, dialect: Dialect, coltype: Any) -> Callable | None: - """No conversion needed for engines that support native bytes.""" - return None - - -# For MariaDB and MySQL we can use an unsigned integer type since it will fit 2**32 -# for sqlite and postgresql we use a bigint -UINT_32_TYPE = BigInteger().with_variant( - mysql.INTEGER(unsigned=True), # type: ignore[no-untyped-call] - "mysql", - "mariadb", -) -JSON_VARIANT_CAST = Text().with_variant( - postgresql.JSON(none_as_null=True), # type: ignore[no-untyped-call] - "postgresql", -) -JSONB_VARIANT_CAST = Text().with_variant( - postgresql.JSONB(none_as_null=True), # type: ignore[no-untyped-call] - "postgresql", -) -DATETIME_TYPE = ( - DateTime(timezone=True) - .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql", "mariadb") # type: ignore[no-untyped-call] - .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") # type: ignore[no-untyped-call] -) -DOUBLE_TYPE = ( - Float() - .with_variant(mysql.DOUBLE(asdecimal=False), "mysql", "mariadb") # type: ignore[no-untyped-call] - .with_variant(oracle.DOUBLE_PRECISION(), "oracle") - .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") -) -UNUSED_LEGACY_COLUMN = Unused(0) -UNUSED_LEGACY_DATETIME_COLUMN = UnusedDateTime(timezone=True) -UNUSED_LEGACY_INTEGER_COLUMN = SmallInteger() -DOUBLE_PRECISION_TYPE_SQL = "DOUBLE PRECISION" -CONTEXT_BINARY_TYPE = LargeBinary(CONTEXT_ID_BIN_MAX_LENGTH).with_variant( - NativeLargeBinary(CONTEXT_ID_BIN_MAX_LENGTH), "mysql", "mariadb", "sqlite" -) - -TIMESTAMP_TYPE = DOUBLE_TYPE - - -class JSONLiteral(JSON): - """Teach SA how to literalize json.""" - - def literal_processor(self, dialect: Dialect) -> Callable[[Any], str]: - """Processor to convert a value to JSON.""" - - def process(value: Any) -> str: - """Dump json.""" - return JSON_DUMP(value) - - return process - - -EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] -EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} - - -class Events(Base): - """Event history data.""" - - __table_args__ = ( - # Used for fetching events at a specific time - # see logbook - Index( - "ix_events_event_type_id_time_fired_ts", "event_type_id", "time_fired_ts" - ), - Index( - EVENTS_CONTEXT_ID_BIN_INDEX, - "context_id_bin", - mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, - mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, - ), - _DEFAULT_TABLE_ARGS, - ) - __tablename__ = TABLE_EVENTS - event_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - event_type: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - event_data: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - origin: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - origin_idx: Mapped[int | None] = mapped_column(SmallInteger) - time_fired: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) - time_fired_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) - context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - data_id: Mapped[int | None] = mapped_column( - Integer, ForeignKey("event_data.data_id"), index=True - ) - context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) - context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) - context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) - event_type_id: Mapped[int | None] = mapped_column( - Integer, ForeignKey("event_types.event_type_id") - ) - event_data_rel: Mapped[EventData | None] = relationship("EventData") - event_type_rel: Mapped[EventTypes | None] = relationship("EventTypes") - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - "" - ) - - @property - def _time_fired_isotime(self) -> str | None: - """Return time_fired as an isotime string.""" - date_time: datetime | None - if self.time_fired_ts is not None: - date_time = dt_util.utc_from_timestamp(self.time_fired_ts) - else: - date_time = process_timestamp(self.time_fired) - if date_time is None: - return None - return date_time.isoformat(sep=" ", timespec="seconds") - - @staticmethod - def from_event(event: Event) -> Events: - """Create an event database object from a native event.""" - return Events( - event_type=None, - event_data=None, - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - time_fired=None, - time_fired_ts=event.time_fired_timestamp, - context_id=None, - context_id_bin=ulid_to_bytes_or_none(event.context.id), - context_user_id=None, - context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), - context_parent_id=None, - context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), - ) - - def to_native(self, validate_entity_id: bool = True) -> Event | None: - """Convert to a native HA Event.""" - context = Context( - id=bytes_to_ulid_or_none(self.context_id_bin), - user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), - parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), - ) - try: - return Event( - self.event_type or "", - json_loads_object(self.event_data) if self.event_data else {}, - EventOrigin(self.origin) - if self.origin - else EVENT_ORIGIN_ORDER[self.origin_idx or 0], - dt_util.utc_from_timestamp(self.time_fired_ts or 0), - context=context, - ) - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting to event: %s", self) - return None - - -class EventData(Base): - """Event data history.""" - - __table_args__ = (_DEFAULT_TABLE_ARGS,) - __tablename__ = TABLE_EVENT_DATA - data_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) - # Note that this is not named attributes to avoid confusion with the states table - shared_data: Mapped[str | None] = mapped_column( - Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") - ) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - "" - ) - - @staticmethod - def shared_data_bytes_from_event( - event: Event, dialect: SupportedDialect | None - ) -> bytes: - """Create shared_data from an event.""" - if dialect == SupportedDialect.POSTGRESQL: - bytes_result = json_bytes_strip_null(event.data) - bytes_result = json_bytes(event.data) - if len(bytes_result) > MAX_EVENT_DATA_BYTES: - _LOGGER.warning( - "Event data for %s exceed maximum size of %s bytes. " - "This can cause database performance issues; Event data " - "will not be stored", - event.event_type, - MAX_EVENT_DATA_BYTES, - ) - return b"{}" - return bytes_result - - @staticmethod - def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: - """Return the hash of json encoded shared data.""" - return fnv1a_32(shared_data_bytes) - - def to_native(self) -> dict[str, Any]: - """Convert to an event data dictionary.""" - shared_data = self.shared_data - if shared_data is None: - return {} - try: - return cast(dict[str, Any], json_loads(shared_data)) - except JSON_DECODE_EXCEPTIONS: - _LOGGER.exception("Error converting row to event data: %s", self) - return {} - - -class EventTypes(Base): - """Event type history.""" - - __table_args__ = (_DEFAULT_TABLE_ARGS,) - __tablename__ = TABLE_EVENT_TYPES - event_type_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - event_type: Mapped[str | None] = mapped_column( - String(MAX_LENGTH_EVENT_EVENT_TYPE), index=True, unique=True - ) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - "" - ) - - -class States(Base): - """State change history.""" - - __table_args__ = ( - # Used for fetching the state of entities at a specific time - # (get_states in history.py) - Index(METADATA_ID_LAST_UPDATED_INDEX_TS, "metadata_id", "last_updated_ts"), - Index( - STATES_CONTEXT_ID_BIN_INDEX, - "context_id_bin", - mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, - mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, - ), - _DEFAULT_TABLE_ARGS, - ) - __tablename__ = TABLE_STATES - state_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - entity_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - state: Mapped[str | None] = mapped_column(String(MAX_LENGTH_STATE_STATE)) - attributes: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - event_id: Mapped[int | None] = mapped_column(UNUSED_LEGACY_INTEGER_COLUMN) - last_changed: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) - last_changed_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) - last_updated: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) - last_updated_ts: Mapped[float | None] = mapped_column( - TIMESTAMP_TYPE, default=time.time, index=True - ) - old_state_id: Mapped[int | None] = mapped_column( - Integer, ForeignKey("states.state_id"), index=True - ) - attributes_id: Mapped[int | None] = mapped_column( - Integer, ForeignKey("state_attributes.attributes_id"), index=True - ) - context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) - origin_idx: Mapped[int | None] = mapped_column( - SmallInteger - ) # 0 is local, 1 is remote - old_state: Mapped[States | None] = relationship("States", remote_side=[state_id]) - state_attributes: Mapped[StateAttributes | None] = relationship("StateAttributes") - context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) - context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) - context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) - metadata_id: Mapped[int | None] = mapped_column( - Integer, ForeignKey("states_meta.metadata_id") - ) - states_meta_rel: Mapped[StatesMeta | None] = relationship("StatesMeta") - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @property - def _last_updated_isotime(self) -> str | None: - """Return last_updated as an isotime string.""" - date_time: datetime | None - if self.last_updated_ts is not None: - date_time = dt_util.utc_from_timestamp(self.last_updated_ts) - else: - date_time = process_timestamp(self.last_updated) - if date_time is None: - return None - return date_time.isoformat(sep=" ", timespec="seconds") - - @staticmethod - def from_event(event: Event) -> States: - """Create object from a state_changed event.""" - state: State | None = event.data.get("new_state") - dbstate = States( - entity_id=None, - attributes=None, - context_id=None, - context_id_bin=ulid_to_bytes_or_none(event.context.id), - context_user_id=None, - context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), - context_parent_id=None, - context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - last_updated=None, - last_changed=None, - ) - # None state means the state was removed from the state machine - if state is None: - dbstate.state = "" - dbstate.last_updated_ts = event.time_fired_timestamp - dbstate.last_changed_ts = None - return dbstate - - dbstate.state = state.state - dbstate.last_updated_ts = state.last_updated_timestamp - if state.last_updated == state.last_changed: - dbstate.last_changed_ts = None - else: - dbstate.last_changed_ts = state.last_changed_timestamp - - return dbstate - - def to_native(self, validate_entity_id: bool = True) -> State | None: - """Convert to an HA state object.""" - context = Context( - id=bytes_to_ulid_or_none(self.context_id_bin), - user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), - parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), - ) - try: - attrs = json_loads_object(self.attributes) if self.attributes else {} - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting row to state: %s", self) - return None - if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: - last_changed = last_updated = dt_util.utc_from_timestamp( - self.last_updated_ts or 0 - ) - else: - last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) - last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) - return State( - self.entity_id or "", - self.state, # type: ignore[arg-type] - # Join the state_attributes table on attributes_id to get the attributes - # for newer states - attrs, - last_changed, - last_updated, - context=context, - validate_entity_id=validate_entity_id, - ) - - -class StateAttributes(Base): - """State attribute change history.""" - - __table_args__ = (_DEFAULT_TABLE_ARGS,) - __tablename__ = TABLE_STATE_ATTRIBUTES - attributes_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) - # Note that this is not named attributes to avoid confusion with the states table - shared_attrs: Mapped[str | None] = mapped_column( - Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") - ) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def shared_attrs_bytes_from_event( - event: Event, - dialect: SupportedDialect | None, - ) -> bytes: - """Create shared_attrs from a state_changed event.""" - state: State | None = event.data.get("new_state") - # None state means the state was removed from the state machine - if state is None: - return b"{}" - if state_info := state.state_info: - unrecorded_attributes = state_info["unrecorded_attributes"] - exclude_attrs = { - *ALL_DOMAIN_EXCLUDE_ATTRS, - *unrecorded_attributes, - } - if MATCH_ALL in unrecorded_attributes: - # Don't exclude device class, state class, unit of measurement - # or friendly name when using the MATCH_ALL exclude constant - _exclude_attributes = { - k: v - for k, v in state.attributes.items() - if k - not in ( - ATTR_DEVICE_CLASS, - ATTR_STATE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, - ATTR_FRIENDLY_NAME, - ) - } - exclude_attrs.update(_exclude_attributes) - - else: - exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS - encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes - bytes_result = encoder( - {k: v for k, v in state.attributes.items() if k not in exclude_attrs} - ) - if len(bytes_result) > MAX_STATE_ATTRS_BYTES: - _LOGGER.warning( - "State attributes for %s exceed maximum size of %s bytes. " - "This can cause database performance issues; Attributes " - "will not be stored", - state.entity_id, - MAX_STATE_ATTRS_BYTES, - ) - return b"{}" - return bytes_result - - @staticmethod - def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: - """Return the hash of json encoded shared attributes.""" - return fnv1a_32(shared_attrs_bytes) - - def to_native(self) -> dict[str, Any]: - """Convert to a state attributes dictionary.""" - shared_attrs = self.shared_attrs - if shared_attrs is None: - return {} - try: - return cast(dict[str, Any], json_loads(shared_attrs)) - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting row to state attributes: %s", self) - return {} - - -class StatesMeta(Base): - """Metadata for states.""" - - __table_args__ = (_DEFAULT_TABLE_ARGS,) - __tablename__ = TABLE_STATES_META - metadata_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - entity_id: Mapped[str | None] = mapped_column( - String(MAX_LENGTH_STATE_ENTITY_ID), index=True, unique=True - ) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - "" - ) - - -class StatisticsBase: - """Statistics base class.""" - - id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - created: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) - created_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, default=time.time) - metadata_id: Mapped[int | None] = mapped_column( - Integer, - ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), - ) - start: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) - start_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) - mean: Mapped[float | None] = mapped_column(DOUBLE_TYPE) - min: Mapped[float | None] = mapped_column(DOUBLE_TYPE) - max: Mapped[float | None] = mapped_column(DOUBLE_TYPE) - last_reset: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) - last_reset_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) - state: Mapped[float | None] = mapped_column(DOUBLE_TYPE) - sum: Mapped[float | None] = mapped_column(DOUBLE_TYPE) - - duration: timedelta - - @classmethod - def from_stats(cls, metadata_id: int, stats: StatisticData) -> Self: - """Create object from a statistics with datatime objects.""" - return cls( # type: ignore[call-arg] - metadata_id=metadata_id, - created=None, - created_ts=time.time(), - start=None, - start_ts=stats["start"].timestamp(), - mean=stats.get("mean"), - min=stats.get("min"), - max=stats.get("max"), - last_reset=None, - last_reset_ts=datetime_to_timestamp_or_none(stats.get("last_reset")), - state=stats.get("state"), - sum=stats.get("sum"), - ) - - @classmethod - def from_stats_ts(cls, metadata_id: int, stats: StatisticDataTimestamp) -> Self: - """Create object from a statistics with timestamps.""" - return cls( # type: ignore[call-arg] - metadata_id=metadata_id, - created=None, - created_ts=time.time(), - start=None, - start_ts=stats["start_ts"], - mean=stats.get("mean"), - min=stats.get("min"), - max=stats.get("max"), - last_reset=None, - last_reset_ts=stats.get("last_reset_ts"), - state=stats.get("state"), - sum=stats.get("sum"), - ) - - -class Statistics(Base, StatisticsBase): - """Long term statistics.""" - - duration = timedelta(hours=1) - - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index( - "ix_statistics_statistic_id_start_ts", - "metadata_id", - "start_ts", - unique=True, - ), - ) - __tablename__ = TABLE_STATISTICS - - -class StatisticsShortTerm(Base, StatisticsBase): - """Short term statistics.""" - - duration = timedelta(minutes=5) - - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index( - "ix_statistics_short_term_statistic_id_start_ts", - "metadata_id", - "start_ts", - unique=True, - ), - ) - __tablename__ = TABLE_STATISTICS_SHORT_TERM - - -class StatisticsMeta(Base): - """Statistics meta data.""" - - __table_args__ = (_DEFAULT_TABLE_ARGS,) - __tablename__ = TABLE_STATISTICS_META - id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - statistic_id: Mapped[str | None] = mapped_column( - String(255), index=True, unique=True - ) - source: Mapped[str | None] = mapped_column(String(32)) - unit_of_measurement: Mapped[str | None] = mapped_column(String(255)) - has_mean: Mapped[bool | None] = mapped_column(Boolean) - has_sum: Mapped[bool | None] = mapped_column(Boolean) - name: Mapped[str | None] = mapped_column(String(255)) - - @staticmethod - def from_meta(meta: StatisticMetaData) -> StatisticsMeta: - """Create object from meta data.""" - return StatisticsMeta(**meta) - - -class RecorderRuns(Base): - """Representation of recorder run.""" - - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) - __tablename__ = TABLE_RECORDER_RUNS - run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) - end: Mapped[datetime | None] = mapped_column(DATETIME_TYPE) - closed_incorrect: Mapped[bool] = mapped_column(Boolean, default=False) - created: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - end = ( - f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None - ) - return ( - f"" - ) - - def to_native(self, validate_entity_id: bool = True) -> Self: - """Return self, native format is this model.""" - return self - - -class SchemaChanges(Base): - """Representation of schema version changes.""" - - __tablename__ = TABLE_SCHEMA_CHANGES - change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - schema_version: Mapped[int | None] = mapped_column(Integer) - changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - "" - ) - - -class StatisticsRuns(Base): - """Representation of statistics run.""" - - __tablename__ = TABLE_STATISTICS_RUNS - run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) - start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - -EVENT_DATA_JSON = type_coerce( - EventData.shared_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) -) -OLD_FORMAT_EVENT_DATA_JSON = type_coerce( - Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) -) - -SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) -) -OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) -) - -ENTITY_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["entity_id"] -OLD_ENTITY_ID_IN_EVENT: ColumnElement = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] -DEVICE_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["device_id"] -OLD_STATE = aliased(States, name="old_state") - -SHARED_ATTR_OR_LEGACY_ATTRIBUTES = case( - (StateAttributes.shared_attrs.is_(None), States.attributes), - else_=StateAttributes.shared_attrs, -).label("attributes") -SHARED_DATA_OR_LEGACY_EVENT_DATA = case( - (EventData.shared_data.is_(None), Events.event_data), else_=EventData.shared_data -).label("event_data") diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py deleted file mode 100644 index 20d0c162d35e61..00000000000000 --- a/tests/components/recorder/test_history_db_schema_42.py +++ /dev/null @@ -1,1022 +0,0 @@ -"""The tests the History component.""" - -from __future__ import annotations - -from collections.abc import Generator -from copy import copy -from datetime import datetime, timedelta -import json -from unittest.mock import sentinel - -from freezegun import freeze_time -import pytest - -from homeassistant import core as ha -from homeassistant.components import recorder -from homeassistant.components.recorder import Recorder, history -from homeassistant.components.recorder.filters import Filters -from homeassistant.components.recorder.models import process_timestamp -from homeassistant.components.recorder.util import session_scope -from homeassistant.core import HomeAssistant, State -from homeassistant.helpers.json import JSONEncoder -from homeassistant.util import dt as dt_util - -from .common import ( - assert_dict_of_states_equal_without_context_and_last_changed, - assert_multiple_states_equal_without_context, - assert_multiple_states_equal_without_context_and_last_changed, - assert_states_equal_without_context, - async_recorder_block_till_done, - async_wait_recording_done, - old_db_schema, -) -from .db_schema_42 import StateAttributes, States, StatesMeta - -from tests.typing import RecorderInstanceContextManager - - -@pytest.fixture -async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceContextManager, -) -> None: - """Set up recorder.""" - - -@pytest.fixture(autouse=True) -def db_schema_42(hass: HomeAssistant) -> Generator[None]: - """Fixture to initialize the db with the old schema 42.""" - with old_db_schema(hass, "42"): - yield - - -@pytest.fixture(autouse=True) -def setup_recorder(db_schema_42, recorder_mock: Recorder) -> recorder.Recorder: - """Set up recorder.""" - - -async def test_get_full_significant_states_with_session_entity_no_matches( - hass: HomeAssistant, -) -> None: - """Test getting states at a specific point in time for entities that never have been recorded.""" - now = dt_util.utcnow() - time_before_recorder_ran = now - timedelta(days=1000) - with session_scope(hass=hass, read_only=True) as session: - assert ( - history.get_full_significant_states_with_session( - hass, session, time_before_recorder_ran, now, entity_ids=["demo.id"] - ) - == {} - ) - assert ( - history.get_full_significant_states_with_session( - hass, - session, - time_before_recorder_ran, - now, - entity_ids=["demo.id", "demo.id2"], - ) - == {} - ) - - -async def test_significant_states_with_session_entity_minimal_response_no_matches( - hass: HomeAssistant, -) -> None: - """Test getting states at a specific point in time for entities that never have been recorded.""" - now = dt_util.utcnow() - time_before_recorder_ran = now - timedelta(days=1000) - with session_scope(hass=hass, read_only=True) as session: - assert ( - history.get_significant_states_with_session( - hass, - session, - time_before_recorder_ran, - now, - entity_ids=["demo.id"], - minimal_response=True, - ) - == {} - ) - assert ( - history.get_significant_states_with_session( - hass, - session, - time_before_recorder_ran, - now, - entity_ids=["demo.id", "demo.id2"], - minimal_response=True, - ) - == {} - ) - - -async def test_significant_states_with_session_single_entity( - hass: HomeAssistant, -) -> None: - """Test get_significant_states_with_session with a single entity.""" - hass.states.async_set("demo.id", "any", {"attr": True}) - hass.states.async_set("demo.id", "any2", {"attr": True}) - await async_wait_recording_done(hass) - now = dt_util.utcnow() - with session_scope(hass=hass, read_only=True) as session: - states = history.get_significant_states_with_session( - hass, - session, - now - timedelta(days=1), - now, - entity_ids=["demo.id"], - minimal_response=False, - ) - assert len(states["demo.id"]) == 2 - - -@pytest.mark.parametrize( - ("attributes", "no_attributes", "limit"), - [ - ({"attr": True}, False, 5000), - ({}, True, 5000), - ({"attr": True}, False, 3), - ({}, True, 3), - ], -) -async def test_state_changes_during_period( - hass: HomeAssistant, attributes, no_attributes, limit -) -> None: - """Test state change during period.""" - entity_id = "media_player.test" - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state, attributes) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - point = start + timedelta(seconds=1) - end = point + timedelta(seconds=1) - - with freeze_time(start) as freezer: - set_state("idle") - set_state("YouTube") - - freezer.move_to(point) - states = [ - set_state("idle"), - set_state("Netflix"), - set_state("Plex"), - set_state("YouTube"), - ] - - freezer.move_to(end) - set_state("Netflix") - set_state("Plex") - await async_wait_recording_done(hass) - - hist = history.state_changes_during_period( - hass, start, end, entity_id, no_attributes, limit=limit - ) - - assert_multiple_states_equal_without_context(states[:limit], hist[entity_id]) - - -async def test_state_changes_during_period_last_reported( - hass: HomeAssistant, -) -> None: - """Test state change during period.""" - entity_id = "media_player.test" - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state) - return ha.State.from_dict(hass.states.get(entity_id).as_dict()) - - start = dt_util.utcnow() - point1 = start + timedelta(seconds=1) - point2 = point1 + timedelta(seconds=1) - end = point2 + timedelta(seconds=1) - - with freeze_time(start) as freezer: - set_state("idle") - - freezer.move_to(point1) - states = [set_state("YouTube")] - - freezer.move_to(point2) - set_state("YouTube") - - freezer.move_to(end) - set_state("Netflix") - await async_wait_recording_done(hass) - - hist = history.state_changes_during_period(hass, start, end, entity_id) - - assert_multiple_states_equal_without_context(states, hist[entity_id]) - - -async def test_state_changes_during_period_descending( - hass: HomeAssistant, -) -> None: - """Test state change during period descending.""" - entity_id = "media_player.test" - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state, {"any": 1}) - return hass.states.get(entity_id) - - start = dt_util.utcnow().replace(microsecond=0) - point = start + timedelta(seconds=1) - point2 = start + timedelta(seconds=1, microseconds=100) - point3 = start + timedelta(seconds=1, microseconds=200) - point4 = start + timedelta(seconds=1, microseconds=300) - end = point + timedelta(seconds=1, microseconds=400) - - with freeze_time(start) as freezer: - set_state("idle") - set_state("YouTube") - - freezer.move_to(point) - states = [set_state("idle")] - - freezer.move_to(point2) - states.append(set_state("Netflix")) - - freezer.move_to(point3) - states.append(set_state("Plex")) - - freezer.move_to(point4) - states.append(set_state("YouTube")) - - freezer.move_to(end) - set_state("Netflix") - set_state("Plex") - await async_wait_recording_done(hass) - - hist = history.state_changes_during_period( - hass, start, end, entity_id, no_attributes=False, descending=False - ) - - assert_multiple_states_equal_without_context(states, hist[entity_id]) - - hist = history.state_changes_during_period( - hass, start, end, entity_id, no_attributes=False, descending=True - ) - assert_multiple_states_equal_without_context( - states, list(reversed(list(hist[entity_id]))) - ) - - start_time = point2 + timedelta(microseconds=10) - hist = history.state_changes_during_period( - hass, - start_time, # Pick a point where we will generate a start time state - end, - entity_id, - no_attributes=False, - descending=True, - include_start_time_state=True, - ) - hist_states = list(hist[entity_id]) - assert hist_states[-1].last_updated == start_time - assert hist_states[-1].last_changed == start_time - assert len(hist_states) == 3 - # Make sure they are in descending order - assert ( - hist_states[0].last_updated - > hist_states[1].last_updated - > hist_states[2].last_updated - ) - assert ( - hist_states[0].last_changed - > hist_states[1].last_changed - > hist_states[2].last_changed - ) - hist = history.state_changes_during_period( - hass, - start_time, # Pick a point where we will generate a start time state - end, - entity_id, - no_attributes=False, - descending=False, - include_start_time_state=True, - ) - hist_states = list(hist[entity_id]) - assert hist_states[0].last_updated == start_time - assert hist_states[0].last_changed == start_time - assert len(hist_states) == 3 - # Make sure they are in ascending order - assert ( - hist_states[0].last_updated - < hist_states[1].last_updated - < hist_states[2].last_updated - ) - assert ( - hist_states[0].last_changed - < hist_states[1].last_changed - < hist_states[2].last_changed - ) - - -async def test_get_last_state_changes(hass: HomeAssistant) -> None: - """Test number of state changes.""" - entity_id = "sensor.test" - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1, seconds=1) - states = [] - - with freeze_time(start) as freezer: - set_state("1") - - freezer.move_to(point) - states.append(set_state("2")) - - freezer.move_to(point2) - states.append(set_state("3")) - await async_wait_recording_done(hass) - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert_multiple_states_equal_without_context(states, hist[entity_id]) - - -async def test_get_last_state_changes_last_reported( - hass: HomeAssistant, -) -> None: - """Test number of state changes.""" - entity_id = "sensor.test" - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state) - return ha.State.from_dict(hass.states.get(entity_id).as_dict()) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1, seconds=1) - states = [] - - with freeze_time(start) as freezer: - states.append(set_state("1")) - - freezer.move_to(point) - set_state("1") - - freezer.move_to(point2) - states.append(set_state("2")) - await async_wait_recording_done(hass) - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert_multiple_states_equal_without_context(states, hist[entity_id]) - - -async def test_get_last_state_change(hass: HomeAssistant) -> None: - """Test getting the last state change for an entity.""" - entity_id = "sensor.test" - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1, seconds=1) - states = [] - - with freeze_time(start) as freezer: - set_state("1") - - freezer.move_to(point) - set_state("2") - - freezer.move_to(point2) - states.append(set_state("3")) - await async_wait_recording_done(hass) - - hist = history.get_last_state_changes(hass, 1, entity_id) - - assert_multiple_states_equal_without_context(states, hist[entity_id]) - - -async def test_ensure_state_can_be_copied( - hass: HomeAssistant, -) -> None: - """Ensure a state can pass though copy(). - - The filter integration uses copy() on states - from history. - """ - entity_id = "sensor.test" - - def set_state(state): - """Set the state.""" - hass.states.async_set(entity_id, state) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - - with freeze_time(start) as freezer: - set_state("1") - - freezer.move_to(point) - set_state("2") - await async_wait_recording_done(hass) - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert_states_equal_without_context(copy(hist[entity_id][0]), hist[entity_id][0]) - assert_states_equal_without_context(copy(hist[entity_id][1]), hist[entity_id][1]) - - -async def test_get_significant_states(hass: HomeAssistant) -> None: - """Test that only significant states are returned. - - We should get back every thermostat change that - includes an attribute change, but only the state updates for - media player (attribute changes are not significant and not returned). - """ - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - hist = history.get_significant_states(hass, zero, four, entity_ids=list(states)) - assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - - -async def test_get_significant_states_minimal_response( - hass: HomeAssistant, -) -> None: - """Test that only significant states are returned. - - When minimal responses is set only the first and - last states return a complete state. - - We should get back every thermostat change that - includes an attribute change, but only the state updates for - media player (attribute changes are not significant and not returned). - """ - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - hist = history.get_significant_states( - hass, zero, four, minimal_response=True, entity_ids=list(states) - ) - entites_with_reducable_states = [ - "media_player.test", - "media_player.test3", - ] - - # All states for media_player.test state are reduced - # down to last_changed and state when minimal_response - # is set except for the first state. - # is set. We use JSONEncoder to make sure that are - # pre-encoded last_changed is always the same as what - # will happen with encoding a native state - for entity_id in entites_with_reducable_states: - entity_states = states[entity_id] - for state_idx in range(1, len(entity_states)): - input_state = entity_states[state_idx] - orig_last_changed = json.dumps( - process_timestamp(input_state.last_changed), - cls=JSONEncoder, - ).replace('"', "") - orig_state = input_state.state - entity_states[state_idx] = { - "last_changed": orig_last_changed, - "state": orig_state, - } - - assert len(hist) == len(states) - assert_states_equal_without_context( - states["media_player.test"][0], hist["media_player.test"][0] - ) - assert states["media_player.test"][1] == hist["media_player.test"][1] - assert states["media_player.test"][2] == hist["media_player.test"][2] - - assert_multiple_states_equal_without_context( - states["media_player.test2"], hist["media_player.test2"] - ) - assert_states_equal_without_context( - states["media_player.test3"][0], hist["media_player.test3"][0] - ) - assert states["media_player.test3"][1] == hist["media_player.test3"][1] - - assert_multiple_states_equal_without_context( - states["script.can_cancel_this_one"], hist["script.can_cancel_this_one"] - ) - assert_multiple_states_equal_without_context_and_last_changed( - states["thermostat.test"], hist["thermostat.test"] - ) - assert_multiple_states_equal_without_context_and_last_changed( - states["thermostat.test2"], hist["thermostat.test2"] - ) - - -@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "US/Hawaii", "UTC"]) -async def test_get_significant_states_with_initial( - time_zone, hass: HomeAssistant -) -> None: - """Test that only significant states are returned. - - We should get back every thermostat change that - includes an attribute change, but only the state updates for - media player (attribute changes are not significant and not returned). - """ - await hass.config.async_set_time_zone(time_zone) - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - one_and_half = zero + timedelta(seconds=1.5) - for entity_id in states: - if entity_id == "media_player.test": - states[entity_id] = states[entity_id][1:] - for state in states[entity_id]: - # If the state is recorded before the start time - # start it will have its last_updated and last_changed - # set to the start time. - if state.last_updated < one_and_half: - state.last_updated = one_and_half - state.last_changed = one_and_half - - hist = history.get_significant_states( - hass, one_and_half, four, include_start_time_state=True, entity_ids=list(states) - ) - assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - - -async def test_get_significant_states_without_initial( - hass: HomeAssistant, -) -> None: - """Test that only significant states are returned. - - We should get back every thermostat change that - includes an attribute change, but only the state updates for - media player (attribute changes are not significant and not returned). - """ - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - one = zero + timedelta(seconds=1) - one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) - one_and_half = zero + timedelta(seconds=1.5) - for entity_id in states: - states[entity_id] = [ - s - for s in states[entity_id] - if s.last_changed not in (one, one_with_microsecond) - ] - del states["media_player.test2"] - del states["thermostat.test3"] - - hist = history.get_significant_states( - hass, - one_and_half, - four, - include_start_time_state=False, - entity_ids=list(states), - ) - assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - - -async def test_get_significant_states_entity_id( - hass: HomeAssistant, -) -> None: - """Test that only significant states are returned for one entity.""" - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - del states["media_player.test2"] - del states["media_player.test3"] - del states["thermostat.test"] - del states["thermostat.test2"] - del states["thermostat.test3"] - del states["script.can_cancel_this_one"] - - hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) - assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - - -async def test_get_significant_states_multiple_entity_ids( - hass: HomeAssistant, -) -> None: - """Test that only significant states are returned for one entity.""" - zero, four, states = record_states(hass) - await async_wait_recording_done(hass) - - hist = history.get_significant_states( - hass, - zero, - four, - ["media_player.test", "thermostat.test"], - ) - - assert_multiple_states_equal_without_context_and_last_changed( - states["media_player.test"], hist["media_player.test"] - ) - assert_multiple_states_equal_without_context_and_last_changed( - states["thermostat.test"], hist["thermostat.test"] - ) - - -async def test_get_significant_states_are_ordered( - hass: HomeAssistant, -) -> None: - """Test order of results from get_significant_states. - - When entity ids are given, the results should be returned with the data - in the same order. - """ - zero, four, _states = record_states(hass) - await async_wait_recording_done(hass) - - entity_ids = ["media_player.test", "media_player.test2"] - hist = history.get_significant_states(hass, zero, four, entity_ids) - assert list(hist.keys()) == entity_ids - entity_ids = ["media_player.test2", "media_player.test"] - hist = history.get_significant_states(hass, zero, four, entity_ids) - assert list(hist.keys()) == entity_ids - - -async def test_get_significant_states_only( - hass: HomeAssistant, -) -> None: - """Test significant states when significant_states_only is set.""" - entity_id = "sensor.test" - - def set_state(state, **kwargs): - """Set the state.""" - hass.states.async_set(entity_id, state, **kwargs) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=4) - points = [start + timedelta(minutes=i) for i in range(1, 4)] - - states = [] - with freeze_time(start) as freezer: - set_state("123", attributes={"attribute": 10.64}) - - freezer.move_to(points[0]) - # Attributes are different, state not - states.append(set_state("123", attributes={"attribute": 21.42})) - - freezer.move_to(points[1]) - # state is different, attributes not - states.append(set_state("32", attributes={"attribute": 21.42})) - - freezer.move_to(points[2]) - # everything is different - states.append(set_state("412", attributes={"attribute": 54.23})) - await async_wait_recording_done(hass) - - hist = history.get_significant_states( - hass, - start, - significant_changes_only=True, - entity_ids=list({state.entity_id for state in states}), - ) - - assert len(hist[entity_id]) == 2 - assert not any( - state.last_updated == states[0].last_updated for state in hist[entity_id] - ) - assert any( - state.last_updated == states[1].last_updated for state in hist[entity_id] - ) - assert any( - state.last_updated == states[2].last_updated for state in hist[entity_id] - ) - - hist = history.get_significant_states( - hass, - start, - significant_changes_only=False, - entity_ids=list({state.entity_id for state in states}), - ) - - assert len(hist[entity_id]) == 3 - assert_multiple_states_equal_without_context_and_last_changed( - states, hist[entity_id] - ) - - -async def test_get_significant_states_only_minimal_response( - hass: HomeAssistant, -) -> None: - """Test significant states when significant_states_only is True.""" - now = dt_util.utcnow() - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) - await async_wait_recording_done(hass) - - hist = history.get_significant_states( - hass, - now, - minimal_response=True, - significant_changes_only=False, - entity_ids=["sensor.test"], - ) - assert len(hist["sensor.test"]) == 3 - - -def record_states( - hass: HomeAssistant, -) -> tuple[datetime, datetime, dict[str, list[State]]]: - """Record some test states. - - We inject a bunch of state updates from media player, zone and - thermostat. - """ - mp = "media_player.test" - mp2 = "media_player.test2" - mp3 = "media_player.test3" - therm = "thermostat.test" - therm2 = "thermostat.test2" - therm3 = "thermostat.test3" - zone = "zone.home" - script_c = "script.can_cancel_this_one" - - def set_state(entity_id, state, **kwargs): - """Set the state.""" - hass.states.async_set(entity_id, state, **kwargs) - return hass.states.get(entity_id) - - zero = dt_util.utcnow() - one = zero + timedelta(seconds=1) - two = one + timedelta(seconds=1) - three = two + timedelta(seconds=1) - four = three + timedelta(seconds=1) - - states = {therm: [], therm2: [], therm3: [], mp: [], mp2: [], mp3: [], script_c: []} - with freeze_time(one) as freezer: - states[mp].append( - set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) - ) - states[mp2].append( - set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) - ) - states[mp3].append( - set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) - ) - states[therm].append( - set_state(therm, 20, attributes={"current_temperature": 19.5}) - ) - # This state will be updated - set_state(therm3, 20, attributes={"current_temperature": 19.5}) - - freezer.move_to(one + timedelta(microseconds=1)) - states[mp].append( - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) - ) - - freezer.move_to(two) - # This state will be skipped only different in time - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) - # This state will be skipped because domain is excluded - set_state(zone, "zoning") - states[script_c].append( - set_state(script_c, "off", attributes={"can_cancel": True}) - ) - states[therm].append( - set_state(therm, 21, attributes={"current_temperature": 19.8}) - ) - states[therm2].append( - set_state(therm2, 20, attributes={"current_temperature": 19}) - ) - # This state will be updated - set_state(therm3, 20, attributes={"current_temperature": 19.5}) - - freezer.move_to(three) - states[mp].append( - set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) - ) - states[mp3].append( - set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) - ) - # Attributes changed even though state is the same - states[therm].append( - set_state(therm, 21, attributes={"current_temperature": 20}) - ) - states[therm3].append( - set_state(therm3, 20, attributes={"current_temperature": 19.5}) - ) - - return zero, four, states - - -async def test_get_full_significant_states_handles_empty_last_changed( - hass: HomeAssistant, -) -> None: - """Test getting states when last_changed is null.""" - now = dt_util.utcnow() - hass.states.async_set("sensor.one", "on", {"attr": "original"}) - state0 = hass.states.get("sensor.one") - await hass.async_block_till_done() - hass.states.async_set("sensor.one", "on", {"attr": "new"}) - state1 = hass.states.get("sensor.one") - - assert state0.last_changed == state1.last_changed - assert state0.last_updated != state1.last_updated - await async_wait_recording_done(hass) - - def _get_entries(): - with session_scope(hass=hass, read_only=True) as session: - return history.get_full_significant_states_with_session( - hass, - session, - now, - dt_util.utcnow(), - entity_ids=["sensor.one"], - significant_changes_only=False, - ) - - states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) - sensor_one_states: list[State] = states["sensor.one"] - assert_states_equal_without_context(sensor_one_states[0], state0) - assert_states_equal_without_context(sensor_one_states[1], state1) - assert sensor_one_states[0].last_changed == sensor_one_states[1].last_changed - assert sensor_one_states[0].last_updated != sensor_one_states[1].last_updated - - def _fetch_native_states() -> list[State]: - with session_scope(hass=hass, read_only=True) as session: - native_states = [] - db_state_attributes = { - state_attributes.attributes_id: state_attributes - for state_attributes in session.query(StateAttributes) - } - metadata_id_to_entity_id = { - states_meta.metadata_id: states_meta - for states_meta in session.query(StatesMeta) - } - for db_state in session.query(States): - db_state.entity_id = metadata_id_to_entity_id[ - db_state.metadata_id - ].entity_id - state = db_state.to_native() - state.attributes = db_state_attributes[ - db_state.attributes_id - ].to_native() - native_states.append(state) - return native_states - - native_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( - _fetch_native_states - ) - assert_states_equal_without_context(native_sensor_one_states[0], state0) - assert_states_equal_without_context(native_sensor_one_states[1], state1) - assert ( - native_sensor_one_states[0].last_changed - == native_sensor_one_states[1].last_changed - ) - assert ( - native_sensor_one_states[0].last_updated - != native_sensor_one_states[1].last_updated - ) - - def _fetch_db_states() -> list[States]: - with session_scope(hass=hass, read_only=True) as session: - states = list(session.query(States)) - session.expunge_all() - return states - - db_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( - _fetch_db_states - ) - assert db_sensor_one_states[0].last_changed is None - assert db_sensor_one_states[0].last_changed_ts is None - - assert ( - process_timestamp( - dt_util.utc_from_timestamp(db_sensor_one_states[1].last_changed_ts) - ) - == state0.last_changed - ) - assert db_sensor_one_states[0].last_updated_ts is not None - assert db_sensor_one_states[1].last_updated_ts is not None - assert ( - db_sensor_one_states[0].last_updated_ts - != db_sensor_one_states[1].last_updated_ts - ) - - -async def test_state_changes_during_period_multiple_entities_single_test( - hass: HomeAssistant, -) -> None: - """Test state change during period with multiple entities in the same test. - - This test ensures the sqlalchemy query cache does not - generate incorrect results. - """ - start = dt_util.utcnow() - test_entites = {f"sensor.{i}": str(i) for i in range(30)} - for entity_id, value in test_entites.items(): - hass.states.async_set(entity_id, value) - - await async_wait_recording_done(hass) - end = dt_util.utcnow() - - for entity_id, value in test_entites.items(): - hist = history.state_changes_during_period(hass, start, end, entity_id) - assert len(hist) == 1 - assert hist[entity_id][0].state == value - - -@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00") -async def test_get_full_significant_states_past_year_2038( - hass: HomeAssistant, -) -> None: - """Test we can store times past year 2038.""" - past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") - hass.states.async_set("sensor.one", "on", {"attr": "original"}) - state0 = hass.states.get("sensor.one") - await hass.async_block_till_done() - - hass.states.async_set("sensor.one", "on", {"attr": "new"}) - state1 = hass.states.get("sensor.one") - - await async_wait_recording_done(hass) - - def _get_entries(): - with session_scope(hass=hass, read_only=True) as session: - return history.get_full_significant_states_with_session( - hass, - session, - past_2038_time - timedelta(days=365), - past_2038_time + timedelta(days=365), - entity_ids=["sensor.one"], - significant_changes_only=False, - ) - - states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) - sensor_one_states: list[State] = states["sensor.one"] - assert_states_equal_without_context(sensor_one_states[0], state0) - assert_states_equal_without_context(sensor_one_states[1], state1) - assert sensor_one_states[0].last_changed == past_2038_time - assert sensor_one_states[0].last_updated == past_2038_time - - -async def test_get_significant_states_without_entity_ids_raises( - hass: HomeAssistant, -) -> None: - """Test at least one entity id is required for get_significant_states.""" - now = dt_util.utcnow() - with pytest.raises(ValueError, match="entity_ids must be provided"): - history.get_significant_states(hass, now, None) - - -async def test_state_changes_during_period_without_entity_ids_raises( - hass: HomeAssistant, -) -> None: - """Test at least one entity id is required for state_changes_during_period.""" - now = dt_util.utcnow() - with pytest.raises(ValueError, match="entity_id must be provided"): - history.state_changes_during_period(hass, now, None) - - -async def test_get_significant_states_with_filters_raises( - hass: HomeAssistant, -) -> None: - """Test passing filters is no longer supported.""" - now = dt_util.utcnow() - with pytest.raises(NotImplementedError, match="Filters are no longer supported"): - history.get_significant_states( - hass, now, None, ["media_player.test"], Filters() - ) - - -async def test_get_significant_states_with_non_existent_entity_ids_returns_empty( - hass: HomeAssistant, -) -> None: - """Test get_significant_states returns an empty dict when entities not in the db.""" - now = dt_util.utcnow() - assert history.get_significant_states(hass, now, None, ["nonexistent.entity"]) == {} - - -async def test_state_changes_during_period_with_non_existent_entity_ids_returns_empty( - hass: HomeAssistant, -) -> None: - """Test state_changes_during_period returns an empty dict when entities not in the db.""" - now = dt_util.utcnow() - assert ( - history.state_changes_during_period(hass, now, None, "nonexistent.entity") == {} - ) - - -async def test_get_last_state_changes_with_non_existent_entity_ids_returns_empty( - hass: HomeAssistant, -) -> None: - """Test get_last_state_changes returns an empty dict when entities not in the db.""" - assert history.get_last_state_changes(hass, 1, "nonexistent.entity") == {}