From eed4acc7457c33e623fee39de345d59acfd89678 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Sun, 26 Apr 2026 11:40:47 +0200 Subject: [PATCH 1/8] Add radio_frequency platform to Broadlink (#169128) Co-authored-by: Claude --- homeassistant/components/broadlink/const.py | 1 + .../components/broadlink/radio_frequency.py | 132 +++++++++++++++++ .../components/broadlink/strings.json | 8 + .../broadlink/test_radio_frequency.py | 137 ++++++++++++++++++ 4 files changed, 278 insertions(+) create mode 100644 homeassistant/components/broadlink/radio_frequency.py create mode 100644 tests/components/broadlink/test_radio_frequency.py diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index 602a3693b7b35..e5865168fdf30 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -7,6 +7,7 @@ DOMAINS_AND_TYPES = { Platform.CLIMATE: {"HYS"}, Platform.LIGHT: {"LB1", "LB2"}, + Platform.RADIO_FREQUENCY: {"RM4PRO", "RMPRO"}, Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}, Platform.SELECT: {"HYS"}, Platform.SENSOR: { diff --git a/homeassistant/components/broadlink/radio_frequency.py b/homeassistant/components/broadlink/radio_frequency.py new file mode 100644 index 0000000000000..31b83d5dcfb28 --- /dev/null +++ b/homeassistant/components/broadlink/radio_frequency.py @@ -0,0 +1,132 @@ +"""Radio Frequency platform for Broadlink.""" + +from __future__ import annotations + +import logging + +from broadlink.exceptions import BroadlinkException +from rf_protocols import RadioFrequencyCommand + +from homeassistant.components.radio_frequency import RadioFrequencyTransmitterEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .device import BroadlinkDevice +from .entity import BroadlinkEntity + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + +_TICK_US = 32.84 + +_RF_433_TYPE_BYTE = 0xB2 +_RF_315_TYPE_BYTE = 0xB4 + +_RF_433_RANGE = (433_050_000, 434_790_000) +_RF_315_RANGE = (314_950_000, 315_250_000) + +SUPPORTED_FREQUENCY_RANGES: list[tuple[int, int]] = [_RF_433_RANGE, _RF_315_RANGE] + + +def _type_byte_for_frequency(frequency: int) -> int: + """Return the Broadlink RF type byte for a given carrier frequency.""" + if _RF_433_RANGE[0] <= frequency <= _RF_433_RANGE[1]: + return _RF_433_TYPE_BYTE + if _RF_315_RANGE[0] <= frequency <= _RF_315_RANGE[1]: + return _RF_315_TYPE_BYTE + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="frequency_not_supported", + translation_placeholders={"frequency": f"{frequency / 1_000_000:g}"}, + ) + + +def encode_rf_packet( + *, + type_byte: int, + repeat_count: int, + timings_us: list[int], +) -> bytes: + """Encode raw OOK timings as a Broadlink RF pulse-length packet. + + The layout is:: + + byte 0 type byte (0xB2 for 433 MHz, 0xB4 for 315 MHz) + byte 1 repeat count (additional transmissions after the first) + bytes 2..3 payload length (little-endian), counted from byte 4 + bytes 4..N-1 pulses: 1 byte when ticks < 256, otherwise + 0x00 followed by a 2-byte big-endian tick count + + Each pulse is expressed as multiples of 32.84 µs ticks, which is the + timing resolution of the Broadlink RF front-end. + """ + buf = bytearray([type_byte, repeat_count, 0, 0]) + for duration in timings_us: + ticks = round(abs(duration) / _TICK_US) + div, mod = divmod(ticks, 256) + if div: + buf.append(0x00) + buf.append(div) + buf.append(mod) + payload_len = len(buf) - 4 + buf[2] = payload_len & 0xFF + buf[3] = (payload_len >> 8) & 0xFF + return bytes(buf) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up a Broadlink radio frequency transmitter.""" + # Uses legacy hass.data[DOMAIN] pattern + # pylint: disable-next=hass-use-runtime-data + device: BroadlinkDevice = hass.data[DOMAIN].devices[config_entry.entry_id] + async_add_entities([BroadlinkRadioFrequency(device)]) + + +class BroadlinkRadioFrequency(BroadlinkEntity, RadioFrequencyTransmitterEntity): + """Representation of a Broadlink RF transmitter.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, device: BroadlinkDevice) -> None: + """Initialize the entity.""" + super().__init__(device) + self._attr_unique_id = device.unique_id + + @property + def supported_frequency_ranges(self) -> list[tuple[int, int]]: + """Return the Broadlink-supported narrow RF bands.""" + return SUPPORTED_FREQUENCY_RANGES + + async def async_send_command(self, command: RadioFrequencyCommand) -> None: + """Encode an OOK command and transmit it via the Broadlink device.""" + type_byte = _type_byte_for_frequency(command.frequency) + packet = encode_rf_packet( + type_byte=type_byte, + repeat_count=command.repeat_count, + timings_us=command.get_raw_timings(), + ) + _LOGGER.debug( + "Transmitting RF packet: %d bytes on %d Hz (repeat=%d)", + len(packet), + command.frequency, + command.repeat_count, + ) + + device = self._device + try: + await device.async_request(device.api.send_data, packet) + except (BroadlinkException, OSError) as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="transmit_failed", + translation_placeholders={"error": str(err)}, + ) from err diff --git a/homeassistant/components/broadlink/strings.json b/homeassistant/components/broadlink/strings.json index a019f350ec066..d5d91be6b0e78 100644 --- a/homeassistant/components/broadlink/strings.json +++ b/homeassistant/components/broadlink/strings.json @@ -77,5 +77,13 @@ "name": "Total consumption" } } + }, + "exceptions": { + "frequency_not_supported": { + "message": "Broadlink devices cannot transmit on {frequency} MHz" + }, + "transmit_failed": { + "message": "Failed to transmit RF command: {error}" + } } } diff --git a/tests/components/broadlink/test_radio_frequency.py b/tests/components/broadlink/test_radio_frequency.py new file mode 100644 index 0000000000000..21feaa598be3d --- /dev/null +++ b/tests/components/broadlink/test_radio_frequency.py @@ -0,0 +1,137 @@ +"""Tests for the Broadlink radio_frequency platform.""" + +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import MagicMock, call + +from broadlink.exceptions import BroadlinkException +import pytest +from rf_protocols import OOKCommand + +from homeassistant.components import radio_frequency +from homeassistant.components.broadlink.const import DOMAIN +from homeassistant.components.broadlink.radio_frequency import ( + _RF_315_TYPE_BYTE, + _RF_433_TYPE_BYTE, + _TICK_US, + encode_rf_packet, +) +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util + +from . import get_device + +from tests.common import async_fire_time_changed + +_FREQ_433 = 433_920_000 +_FREQ_315 = 315_000_000 + + +async def _setup_rf_device(hass: HomeAssistant) -> tuple[MagicMock, str]: + """Set up the RMPRO test device, return its api mock and RF entity_id.""" + device = get_device("Office") + mock_setup = await device.setup_entry(hass) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_setup.entry.unique_id)} + ) + entries = er.async_entries_for_device(entity_registry, device_entry.id) + rf_entity = next(e for e in entries if e.domain == Platform.RADIO_FREQUENCY) + return mock_setup.api, rf_entity.entity_id + + +@pytest.mark.parametrize( + ("device_name", "has_rf"), + [ + ("Office", True), # RMPRO + ("Garage", True), # RM4PRO + ("Entrance", False), # RMMINI + ], +) +async def test_radio_frequency_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + device_name: str, + has_rf: bool, +) -> None: + """RF entity is created only for RF-capable devices.""" + device = get_device(device_name) + mock_setup = await device.setup_entry(hass) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_setup.entry.unique_id)} + ) + entries = er.async_entries_for_device(entity_registry, device_entry.id) + rf_entities = [e for e in entries if e.domain == Platform.RADIO_FREQUENCY] + assert len(rf_entities) == (1 if has_rf else 0) + + +def test_encode_rf_packet() -> None: + """Pulses are encoded inline below 256 ticks, escape-prefixed above.""" + timings = [round(12 * _TICK_US), round(300 * _TICK_US), round(12 * _TICK_US)] + packet = encode_rf_packet( + type_byte=_RF_433_TYPE_BYTE, repeat_count=3, timings_us=timings + ) + # type byte, repeat count, payload length (le16), 12, escape (00 01 2c), 12 + assert packet == bytes([0xB2, 0x03, 0x05, 0x00, 0x0C, 0x00, 0x01, 0x2C, 0x0C]) + + +async def test_send_command(hass: HomeAssistant) -> None: + """An OOK command transmits the encoded packet once.""" + api, entity_id = await _setup_rf_device(hass) + + timings = [400, -800, 400, -800] + command = OOKCommand(frequency=_FREQ_433, timings=timings) + await radio_frequency.async_send_command(hass, entity_id, command) + + expected = encode_rf_packet( + type_byte=_RF_433_TYPE_BYTE, repeat_count=0, timings_us=timings + ) + assert api.send_data.call_args == call(expected) + + +async def test_send_command_315_band(hass: HomeAssistant) -> None: + """A 315 MHz command uses the 0xB4 type byte.""" + api, entity_id = await _setup_rf_device(hass) + + command = OOKCommand(frequency=_FREQ_315, timings=[400, -800]) + await radio_frequency.async_send_command(hass, entity_id, command) + + assert api.send_data.call_args.args[0][0] == _RF_315_TYPE_BYTE + + +async def test_send_command_rejects_out_of_band(hass: HomeAssistant) -> None: + """An out-of-band frequency is rejected before send.""" + api, entity_id = await _setup_rf_device(hass) + + command = OOKCommand(frequency=868_000_000, timings=[400, -800]) + with pytest.raises(HomeAssistantError): + await radio_frequency.async_send_command(hass, entity_id, command) + api.send_data.assert_not_called() + + +async def test_send_command_transmit_failure(hass: HomeAssistant) -> None: + """A broadlink exception surfaces as HomeAssistantError.""" + api, entity_id = await _setup_rf_device(hass) + api.send_data.side_effect = BroadlinkException("nope") + + command = OOKCommand(frequency=_FREQ_433, timings=[400, -800]) + with pytest.raises(HomeAssistantError): + await radio_frequency.async_send_command(hass, entity_id, command) + + +async def test_entity_availability(hass: HomeAssistant) -> None: + """Entity becomes unavailable when the device stops responding.""" + api, entity_id = await _setup_rf_device(hass) + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + api.check_sensors.side_effect = OSError("disconnected") + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE From e78a79a29e63d0a340e79921c44879f115f48765 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 26 Apr 2026 13:38:50 +0200 Subject: [PATCH 2/8] Remove `name` from Airly config flow (#169145) Co-authored-by: Copilot --- homeassistant/components/airly/config_flow.py | 21 ++++++++----------- homeassistant/components/airly/const.py | 2 ++ homeassistant/components/airly/sensor.py | 2 +- homeassistant/components/airly/strings.json | 3 +-- tests/components/airly/__init__.py | 1 - .../airly/snapshots/test_diagnostics.ambr | 1 - tests/components/airly/test_config_flow.py | 9 ++++---- tests/components/airly/test_init.py | 6 ------ 8 files changed, 17 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index d3f2240a37c7d..5f0354848a762 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -12,11 +12,11 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS +from .const import CONF_USE_NEAREST, DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS DESCRIPTION_PLACEHOLDERS = { "developer_registration_url": "https://developer.airly.eu/register", @@ -45,16 +45,16 @@ async def async_step_user( try: location_point_valid = await check_location( websession, - user_input["api_key"], - user_input["latitude"], - user_input["longitude"], + user_input[CONF_API_KEY], + user_input[CONF_LATITUDE], + user_input[CONF_LONGITUDE], ) if not location_point_valid: location_nearest_valid = await check_location( websession, - user_input["api_key"], - user_input["latitude"], - user_input["longitude"], + user_input[CONF_API_KEY], + user_input[CONF_LATITUDE], + user_input[CONF_LONGITUDE], use_nearest=True, ) except AirlyError as err: @@ -68,7 +68,7 @@ async def async_step_user( return self.async_abort(reason="wrong_location") use_nearest = True return self.async_create_entry( - title=user_input[CONF_NAME], + title=DEFAULT_NAME, data={**user_input, CONF_USE_NEAREST: use_nearest}, ) @@ -83,9 +83,6 @@ async def async_step_user( vol.Optional( CONF_LONGITUDE, default=self.hass.config.longitude ): cv.longitude, - vol.Optional( - CONF_NAME, default=self.hass.config.location_name - ): str, } ), errors=errors, diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 5939bfa62de24..6dc00ddcc8618 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -37,3 +37,5 @@ MIN_UPDATE_INTERVAL: Final = 5 NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." URL = "https://airly.org/map/#{latitude},{longitude}" + +DEFAULT_NAME: Final = "Airly" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 2aa99d9c792a0..6a99548890885 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -178,7 +178,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Airly sensor entities based on a config entry.""" - name = entry.data[CONF_NAME] + name = entry.data.get(CONF_NAME) or entry.title coordinator = entry.runtime_data diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 6f53c7ed23cb7..f4f98afec27d0 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -13,8 +13,7 @@ "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]", - "name": "[%key:common::config_flow::data::name%]" + "longitude": "[%key:common::config_flow::data::longitude%]" }, "description": "To generate API key go to {developer_registration_url}" } diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 401bf641350cf..199c7a2687022 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -29,7 +29,6 @@ async def init_integration( "api_key": "foo", "latitude": 123, "longitude": 456, - "name": "Home", }, ) diff --git a/tests/components/airly/snapshots/test_diagnostics.ambr b/tests/components/airly/snapshots/test_diagnostics.ambr index 1c760eaec5224..925e48237c1c8 100644 --- a/tests/components/airly/snapshots/test_diagnostics.ambr +++ b/tests/components/airly/snapshots/test_diagnostics.ambr @@ -6,7 +6,6 @@ 'api_key': '**REDACTED**', 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', - 'name': 'Home', }), 'disabled_by': None, 'discovery_keys': dict({ diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 482c97799f6a0..f6687f787492f 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -4,9 +4,9 @@ from airly.exceptions import AirlyError -from homeassistant.components.airly.const import CONF_USE_NEAREST, DOMAIN +from homeassistant.components.airly.const import CONF_USE_NEAREST, DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -16,7 +16,6 @@ from tests.test_util.aiohttp import AiohttpClientMocker CONFIG = { - CONF_NAME: "Home", CONF_API_KEY: "foo", CONF_LATITUDE: 123, CONF_LONGITUDE: 456, @@ -124,7 +123,7 @@ async def test_create_entry( ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == CONFIG[CONF_NAME] + assert result["title"] == DEFAULT_NAME assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] @@ -151,7 +150,7 @@ async def test_create_entry_with_nearest_method( ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == CONFIG[CONF_NAME] + assert result["title"] == DEFAULT_NAME assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index ea24fe80c0aa0..da606d718a3b6 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -43,7 +43,6 @@ async def test_config_not_ready( "api_key": "foo", "latitude": 123, "longitude": 456, - "name": "Home", "use_nearest": True, }, ) @@ -65,7 +64,6 @@ async def test_config_without_unique_id( "api_key": "foo", "latitude": 123, "longitude": 456, - "name": "Home", }, ) @@ -90,7 +88,6 @@ async def test_config_with_turned_off_station( "api_key": "foo", "latitude": 123, "longitude": 456, - "name": "Home", }, ) @@ -122,7 +119,6 @@ async def test_update_interval( "api_key": "foo", "latitude": 123, "longitude": 456, - "name": "Home", }, ) @@ -157,7 +153,6 @@ async def test_update_interval( "api_key": "foo", "latitude": 66.66, "longitude": 111.11, - "name": "Work", }, ) @@ -216,7 +211,6 @@ async def test_migrate_device_entry( "api_key": "foo", "latitude": 123, "longitude": 456, - "name": "Home", }, ) From a506be4be084899f906167edd0c08725eb26bc90 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sun, 26 Apr 2026 14:09:11 +0200 Subject: [PATCH 3/8] Remove TARGET_TEMPERATURE_RANGE from eurotronic climate (#169182) --- .../eurotronic_cometblue/climate.py | 25 +++++++++++-------- .../snapshots/test_climate.ambr | 6 ++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/eurotronic_cometblue/climate.py b/homeassistant/components/eurotronic_cometblue/climate.py index 5df02bec17aa3..f1bb29f788625 100644 --- a/homeassistant/components/eurotronic_cometblue/climate.py +++ b/homeassistant/components/eurotronic_cometblue/climate.py @@ -56,7 +56,6 @@ class CometBlueClimateEntity(CometBlueBluetoothEntity, ClimateEntity): ] _attr_supported_features: ClimateEntityFeature = ( ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF @@ -81,13 +80,19 @@ def target_temperature(self) -> float | None: return self.coordinator.data.temperatures["manualTemp"] @property - def target_temperature_high(self) -> float | None: - """Return the upper bound target temperature.""" + def _device_comfort_setpoint(self) -> float | None: + """Return the comfort setpoint temperature. + + Internally used for preset selection. + """ return self.coordinator.data.temperatures["targetTempHigh"] @property - def target_temperature_low(self) -> float | None: - """Return the lower bound target temperature.""" + def _device_eco_setpoint(self) -> float | None: + """Return the eco setpoint temperature. + + Internally used for preset selection. + """ return self.coordinator.data.temperatures["targetTempLow"] @property @@ -113,9 +118,9 @@ def preset_mode(self) -> str | None: return PRESET_AWAY if self.target_temperature == MAX_TEMP: return PRESET_BOOST - if self.target_temperature == self.target_temperature_high: + if self.target_temperature == self._device_comfort_setpoint: return PRESET_COMFORT - if self.target_temperature == self.target_temperature_low: + if self.target_temperature == self._device_eco_setpoint: return PRESET_ECO return PRESET_NONE @@ -153,11 +158,11 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: ) if preset_mode == PRESET_ECO: return await self.async_set_temperature( - temperature=self.target_temperature_low + temperature=self._device_eco_setpoint ) if preset_mode == PRESET_COMFORT: return await self.async_set_temperature( - temperature=self.target_temperature_high + temperature=self._device_comfort_setpoint ) if preset_mode == PRESET_BOOST: return await self.async_set_temperature(temperature=MAX_TEMP) @@ -172,7 +177,7 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: return await self.async_set_temperature(temperature=MAX_TEMP) if hvac_mode == HVACMode.AUTO: return await self.async_set_temperature( - temperature=self.target_temperature_low + temperature=self._device_eco_setpoint ) raise ServiceValidationError(f"Unknown HVAC mode '{hvac_mode}'") diff --git a/tests/components/eurotronic_cometblue/snapshots/test_climate.ambr b/tests/components/eurotronic_cometblue/snapshots/test_climate.ambr index 88b82ad1a8353..74311182fc2e5 100644 --- a/tests/components/eurotronic_cometblue/snapshots/test_climate.ambr +++ b/tests/components/eurotronic_cometblue/snapshots/test_climate.ambr @@ -46,7 +46,7 @@ 'platform': 'eurotronic_cometblue', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff', 'unit_of_measurement': None, @@ -72,9 +72,7 @@ 'away', 'none', ]), - 'supported_features': , - 'target_temp_high': 21.0, - 'target_temp_low': 17.0, + 'supported_features': , 'target_temp_step': 0.5, 'temperature': 20.0, }), From aa0199b4424b10702c16c77f136ba51afb57d1d6 Mon Sep 17 00:00:00 2001 From: mithomas Date: Sun, 26 Apr 2026 14:14:16 +0200 Subject: [PATCH 4/8] Add LG Netcast service to send remote control commands (#168649) --- .../components/lg_netcast/__init__.py | 2 +- homeassistant/components/lg_netcast/remote.py | 83 +++++++++++++++++++ tests/components/lg_netcast/test_remote.py | 59 +++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lg_netcast/remote.py create mode 100644 tests/components/lg_netcast/test_remote.py diff --git a/homeassistant/components/lg_netcast/__init__.py b/homeassistant/components/lg_netcast/__init__.py index c250988976013..d97464d9a9c10 100644 --- a/homeassistant/components/lg_netcast/__init__.py +++ b/homeassistant/components/lg_netcast/__init__.py @@ -11,7 +11,7 @@ from .const import DOMAIN -PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER] +PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) diff --git a/homeassistant/components/lg_netcast/remote.py b/homeassistant/components/lg_netcast/remote.py new file mode 100644 index 0000000000000..db5562a598e82 --- /dev/null +++ b/homeassistant/components/lg_netcast/remote.py @@ -0,0 +1,83 @@ +"""Remote control support for LG Netcast TV.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any + +from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError +from requests import RequestException + +from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import LgNetCastConfigEntry +from .const import ATTR_MANUFACTURER, DOMAIN + +VALID_COMMANDS: frozenset[str] = frozenset( + k + for k in vars(LG_COMMAND) + if not k.startswith("_") and isinstance(getattr(LG_COMMAND, k), int) +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: LgNetCastConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up LG Netcast Remote from a config entry.""" + client = config_entry.runtime_data + unique_id = config_entry.unique_id + if TYPE_CHECKING: + assert unique_id is not None + + async_add_entities([LgNetCastRemote(client, unique_id)]) + + +class LgNetCastRemote(RemoteEntity): + """Device that sends commands to an LG Netcast TV.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, client: LgNetCastClient, unique_id: str) -> None: + """Initialize the LG Netcast remote.""" + self._client = client + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + ) + + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send commands to the TV.""" + num_repeats = kwargs[ATTR_NUM_REPEATS] + + commands: list[int] = [] + for cmd in command: + if cmd not in VALID_COMMANDS: + raise ServiceValidationError(f"Unknown command: {cmd!r}") + commands.append(getattr(LG_COMMAND, cmd)) + for _ in range(num_repeats): + try: + with self._client as client: + for lg_command in commands: + client.send_command(lg_command) + except LgNetCastError, RequestException: + self._attr_is_on = False + self.schedule_update_ha_state() + return + + def turn_on(self, **kwargs: Any) -> None: + """Turn on is handled via a separate turn_on trigger.""" + raise NotImplementedError( + "Turning on the TV is not supported by the LG Netcast remote entity" + ) + + def turn_off(self, **kwargs: Any) -> None: + """Turn off the TV.""" + self.send_command(["POWER"], **{ATTR_NUM_REPEATS: 1}) diff --git a/tests/components/lg_netcast/test_remote.py b/tests/components/lg_netcast/test_remote.py new file mode 100644 index 0000000000000..facdbe80b0808 --- /dev/null +++ b/tests/components/lg_netcast/test_remote.py @@ -0,0 +1,59 @@ +"""Tests for LG Netcast remote platform.""" + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +from pylgnetcast import LG_COMMAND +import pytest + +from homeassistant.components.remote import ( + ATTR_COMMAND, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from . import MODEL_NAME, setup_lgnetcast + +REMOTE_ENTITY_ID = f"{REMOTE_DOMAIN}.{MODEL_NAME.lower()}" + + +@pytest.fixture(autouse=True) +def mock_lg_netcast() -> Generator[MagicMock]: + """Mock LG Netcast library.""" + with patch( + "homeassistant.components.lg_netcast.LgNetCastClient" + ) as mock_client_class: + yield mock_client_class + + +async def test_send_command(hass: HomeAssistant, mock_lg_netcast: MagicMock) -> None: + """Test remote.send_command calls the client with the correct command code.""" + await setup_lgnetcast(hass) + context_client = mock_lg_netcast.return_value.__enter__.return_value + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: REMOTE_ENTITY_ID, ATTR_COMMAND: ["POWER"]}, + blocking=True, + ) + + context_client.send_command.assert_called_once_with(LG_COMMAND.POWER) + + +async def test_send_command_invalid( + hass: HomeAssistant, mock_lg_netcast: MagicMock +) -> None: + """Test remote.send_command raises ServiceValidationError for an unknown command name.""" + await setup_lgnetcast(hass) + + with pytest.raises(ServiceValidationError, match="Unknown command"): + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: REMOTE_ENTITY_ID, ATTR_COMMAND: ["NOT_A_REAL_COMMAND"]}, + blocking=True, + ) From fa9a336725817c923eee5bcb91d8b4d8ac5d2e32 Mon Sep 17 00:00:00 2001 From: Kurt Chrisford <92524101+kclif9@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:17:24 +1000 Subject: [PATCH 5/8] Bump actron-neo-api to 0.5.5 (#169176) --- homeassistant/components/actron_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/actron_air/fixtures/status.json | 7 +++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/actron_air/manifest.json b/homeassistant/components/actron_air/manifest.json index 8bcf92bb03834..5aee0516b6699 100644 --- a/homeassistant/components/actron_air/manifest.json +++ b/homeassistant/components/actron_air/manifest.json @@ -13,5 +13,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["actron-neo-api==0.5.3"] + "requirements": ["actron-neo-api==0.5.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index bf46e4d05bf18..e86cfeac3f4da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ WSDiscovery==2.1.2 accuweather==5.1.0 # homeassistant.components.actron_air -actron-neo-api==0.5.3 +actron-neo-api==0.5.5 # homeassistant.components.adax adax==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07751fea2bc79..af20fdc51dbf1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -124,7 +124,7 @@ WSDiscovery==2.1.2 accuweather==5.1.0 # homeassistant.components.actron_air -actron-neo-api==0.5.3 +actron-neo-api==0.5.5 # homeassistant.components.adax adax==0.4.0 diff --git a/tests/components/actron_air/fixtures/status.json b/tests/components/actron_air/fixtures/status.json index fdb5c01dabca9..0f6c9c1f1017e 100644 --- a/tests/components/actron_air/fixtures/status.json +++ b/tests/components/actron_air/fixtures/status.json @@ -22,6 +22,13 @@ "TurboMode": { "Enabled": false, "Supported": true + }, + "ModeSupport": { + "Cool": true, + "Heat": true, + "Fan": true, + "Auto": true, + "Dry": false } }, "MasterInfo": { From cd98577eb7b0469bd38dd53e03a069340b4dbf07 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 26 Apr 2026 14:23:59 +0200 Subject: [PATCH 6/8] Update easyEnergy integration to v3.0.0 (#169162) --- .../components/easyenergy/diagnostics.py | 25 +- .../components/easyenergy/manifest.json | 2 +- homeassistant/components/easyenergy/sensor.py | 29 +- .../components/easyenergy/services.py | 124 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/easyenergy/conftest.py | 13 +- .../easyenergy/fixtures/today_energy.json | 463 ++-- .../easyenergy/fixtures/today_gas.json | 177 +- .../snapshots/test_diagnostics.ambr | 68 +- .../easyenergy/snapshots/test_services.ambr | 2304 ++++++----------- .../components/easyenergy/test_diagnostics.py | 4 +- tests/components/easyenergy/test_sensor.py | 30 +- tests/components/easyenergy/test_services.py | 253 +- 14 files changed, 1526 insertions(+), 1970 deletions(-) diff --git a/homeassistant/components/easyenergy/diagnostics.py b/homeassistant/components/easyenergy/diagnostics.py index 64f30ba61fdac..55a3614e495ca 100644 --- a/homeassistant/components/easyenergy/diagnostics.py +++ b/homeassistant/components/easyenergy/diagnostics.py @@ -6,6 +6,7 @@ from typing import Any from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from .coordinator import EasyEnergyConfigEntry, EasyEnergyData @@ -23,9 +24,7 @@ def get_gas_price(data: EasyEnergyData, hours: int) -> float | None: """ if not data.gas_today: return None - return data.gas_today.price_at_time( - data.gas_today.utcnow() + timedelta(hours=hours) - ) + return data.gas_today.price_at_time(dt_util.utcnow() + timedelta(hours=hours)) async def async_get_config_entry_diagnostics( @@ -40,21 +39,21 @@ async def async_get_config_entry_diagnostics( "title": entry.title, }, "energy_usage": { - "current_hour_price": energy_today.current_usage_price, + "current_hour_price": energy_today.current_price, "next_hour_price": energy_today.price_at_time( - energy_today.utcnow() + timedelta(hours=1) + dt_util.utcnow() + timedelta(hours=1) ), - "average_price": energy_today.average_usage_price, - "max_price": energy_today.extreme_usage_prices[1], - "min_price": energy_today.extreme_usage_prices[0], - "highest_price_time": energy_today.highest_usage_price_time, - "lowest_price_time": energy_today.lowest_usage_price_time, - "percentage_of_max": energy_today.pct_of_max_usage, + "average_price": energy_today.average_price, + "max_price": energy_today.extreme_prices[1], + "min_price": energy_today.extreme_prices[0], + "highest_price_time": energy_today.highest_price_time, + "lowest_price_time": energy_today.lowest_price_time, + "percentage_of_max": energy_today.pct_of_max, }, "energy_return": { "current_hour_price": energy_today.current_return_price, - "next_hour_price": energy_today.price_at_time( - energy_today.utcnow() + timedelta(hours=1), "return" + "next_hour_price": energy_today.return_price_at_time( + dt_util.utcnow() + timedelta(hours=1) ), "average_price": energy_today.average_return_price, "max_price": energy_today.extreme_return_prices[1], diff --git a/homeassistant/components/easyenergy/manifest.json b/homeassistant/components/easyenergy/manifest.json index c987e75e7180f..7c0c00f760752 100644 --- a/homeassistant/components/easyenergy/manifest.json +++ b/homeassistant/components/easyenergy/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/easyenergy", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["easyenergy==2.2.0"], + "requirements": ["easyenergy==3.0.0"], "single_config_entry": true } diff --git a/homeassistant/components/easyenergy/sensor.py b/homeassistant/components/easyenergy/sensor.py index 35fab870af381..c0638344cb656 100644 --- a/homeassistant/components/easyenergy/sensor.py +++ b/homeassistant/components/easyenergy/sensor.py @@ -24,6 +24,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util from .const import DOMAIN, SERVICE_TYPE_DEVICE_NAMES from .coordinator import ( @@ -63,7 +64,7 @@ class EasyEnergySensorEntityDescription(SensorEntityDescription): service_type="today_energy_usage", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", - value_fn=lambda data: data.energy_today.current_usage_price, + value_fn=lambda data: data.energy_today.current_price, ), EasyEnergySensorEntityDescription( key="next_hour_price", @@ -71,7 +72,7 @@ class EasyEnergySensorEntityDescription(SensorEntityDescription): service_type="today_energy_usage", native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", value_fn=lambda data: data.energy_today.price_at_time( - data.energy_today.utcnow() + timedelta(hours=1) + dt_util.utcnow() + timedelta(hours=1) ), ), EasyEnergySensorEntityDescription( @@ -79,42 +80,42 @@ class EasyEnergySensorEntityDescription(SensorEntityDescription): translation_key="average_price", service_type="today_energy_usage", native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", - value_fn=lambda data: data.energy_today.average_usage_price, + value_fn=lambda data: data.energy_today.average_price, ), EasyEnergySensorEntityDescription( key="max_price", translation_key="max_price", service_type="today_energy_usage", native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", - value_fn=lambda data: data.energy_today.extreme_usage_prices[1], + value_fn=lambda data: data.energy_today.extreme_prices[1], ), EasyEnergySensorEntityDescription( key="min_price", translation_key="min_price", service_type="today_energy_usage", native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", - value_fn=lambda data: data.energy_today.extreme_usage_prices[0], + value_fn=lambda data: data.energy_today.extreme_prices[0], ), EasyEnergySensorEntityDescription( key="highest_price_time", translation_key="highest_price_time", service_type="today_energy_usage", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: data.energy_today.highest_usage_price_time, + value_fn=lambda data: data.energy_today.highest_price_time, ), EasyEnergySensorEntityDescription( key="lowest_price_time", translation_key="lowest_price_time", service_type="today_energy_usage", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: data.energy_today.lowest_usage_price_time, + value_fn=lambda data: data.energy_today.lowest_price_time, ), EasyEnergySensorEntityDescription( key="percentage_of_max", translation_key="percentage_of_max", service_type="today_energy_usage", native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: data.energy_today.pct_of_max_usage, + value_fn=lambda data: data.energy_today.pct_of_max, ), EasyEnergySensorEntityDescription( key="current_hour_price", @@ -129,8 +130,8 @@ class EasyEnergySensorEntityDescription(SensorEntityDescription): translation_key="next_hour_price", service_type="today_energy_return", native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", - value_fn=lambda data: data.energy_today.price_at_time( - data.energy_today.utcnow() + timedelta(hours=1), "return" + value_fn=lambda data: data.energy_today.return_price_at_time( + dt_util.utcnow() + timedelta(hours=1) ), ), EasyEnergySensorEntityDescription( @@ -180,14 +181,14 @@ class EasyEnergySensorEntityDescription(SensorEntityDescription): translation_key="hours_priced_equal_or_lower", service_type="today_energy_usage", native_unit_of_measurement=UnitOfTime.HOURS, - value_fn=lambda data: data.energy_today.hours_priced_equal_or_lower_usage, + value_fn=lambda data: data.energy_today.periods_priced_equal_or_lower, ), EasyEnergySensorEntityDescription( key="hours_priced_equal_or_higher", translation_key="hours_priced_equal_or_higher", service_type="today_energy_return", native_unit_of_measurement=UnitOfTime.HOURS, - value_fn=lambda data: data.energy_today.hours_priced_equal_or_higher_return, + value_fn=lambda data: data.energy_today.return_periods_priced_equal_or_higher, ), ) @@ -205,9 +206,7 @@ def get_gas_price(data: EasyEnergyData, hours: int) -> float | None: """ if data.gas_today is None: return None - return data.gas_today.price_at_time( - data.gas_today.utcnow() + timedelta(hours=hours) - ) + return data.gas_today.price_at_time(dt_util.utcnow() + timedelta(hours=hours)) async def async_setup_entry( diff --git a/homeassistant/components/easyenergy/services.py b/homeassistant/components/easyenergy/services.py index 1ae7d5c5b5a72..f6886f6df4efd 100644 --- a/homeassistant/components/easyenergy/services.py +++ b/homeassistant/components/easyenergy/services.py @@ -2,12 +2,13 @@ from __future__ import annotations -from datetime import date, datetime +from datetime import date, datetime, timedelta from enum import StrEnum from functools import partial from typing import Final -from easyenergy import Electricity, Gas, VatOption +from easyenergy import Electricity, Gas, PriceInterval, VatOption +from easyenergy.const import MARKET_TIMEZONE import voluptuous as vol from homeassistant.core import ( @@ -32,18 +33,22 @@ GAS_SERVICE_NAME: Final = "get_gas_prices" ENERGY_USAGE_SERVICE_NAME: Final = "get_energy_usage_prices" ENERGY_RETURN_SERVICE_NAME: Final = "get_energy_return_prices" +BASE_SERVICE_SCHEMA: Final = { + vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), + vol.Optional(ATTR_START): str, + vol.Optional(ATTR_END): str, +} SERVICE_SCHEMA: Final = vol.Schema( { - vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( - { - "integration": DOMAIN, - } - ), + **BASE_SERVICE_SCHEMA, vol.Required(ATTR_INCL_VAT): bool, - vol.Optional(ATTR_START): str, - vol.Optional(ATTR_END): str, } ) +RETURN_SERVICE_SCHEMA: Final = vol.Schema(BASE_SERVICE_SCHEMA) class PriceType(StrEnum): @@ -54,22 +59,47 @@ class PriceType(StrEnum): GAS = "gas" -def __get_date(date_input: str | None) -> date | datetime: - """Get date.""" +def __get_date( + date_input: str | None, +) -> tuple[date, datetime | None]: + """Get date for the API and optional datetime for response filtering.""" if not date_input: - return dt_util.now().date() - - if value := dt_util.parse_datetime(date_input): - return value - - raise ServiceValidationError( - "Invalid datetime provided.", - translation_domain=DOMAIN, - translation_key="invalid_date", - translation_placeholders={ - "date": date_input, - }, - ) + return dt_util.now().date(), None + + if date_value := dt_util.parse_date(date_input): + return date_value, None + + if not (datetime_value := dt_util.parse_datetime(date_input)): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_date", + translation_placeholders={ + "date": date_input, + }, + ) + + datetime_utc = dt_util.as_utc(datetime_value) + return datetime_utc.astimezone(MARKET_TIMEZONE).date(), datetime_utc + + +def __filter_prices( + prices: list[dict[str, float | datetime]], + intervals: tuple[PriceInterval, ...], + start: datetime, + end: datetime, +) -> list[dict[str, float | datetime]]: + """Filter prices to the requested datetime range.""" + included_timestamps = { + interval.starts_at + for interval in intervals + if interval.ends_at > start and interval.starts_at < end + } + + return [ + timestamp_price + for timestamp_price in prices + if timestamp_price["timestamp"] in included_timestamps + ] def __serialize_prices(prices: list[dict[str, float | datetime]]) -> ServiceResponse: @@ -101,8 +131,8 @@ async def __get_prices( """Get prices from easyEnergy.""" coordinator = __get_coordinator(call) - start = __get_date(call.data.get(ATTR_START)) - end = __get_date(call.data.get(ATTR_END)) + start_date, start_datetime = __get_date(call.data.get(ATTR_START)) + end_date, end_datetime = __get_date(call.data.get(ATTR_END)) vat = VatOption.INCLUDE if call.data.get(ATTR_INCL_VAT) is False: @@ -112,20 +142,38 @@ async def __get_prices( if price_type == PriceType.GAS: data = await coordinator.easyenergy.gas_prices( - start_date=start, - end_date=end, + start_date=start_date, + end_date=end_date, vat=vat, ) - return __serialize_prices(data.timestamp_prices) - data = await coordinator.easyenergy.energy_prices( - start_date=start, - end_date=end, - vat=vat, - ) + prices = data.timestamp_prices + else: + data = await coordinator.easyenergy.energy_prices( + start_date=start_date, + end_date=end_date, + vat=vat, + ) + + if price_type == PriceType.ENERGY_USAGE: + prices = data.timestamp_prices + else: + prices = data.timestamp_return_prices + + if start_datetime or end_datetime: + filter_start = start_datetime or dt_util.as_utc( + dt_util.start_of_local_day(start_date) + ) + filter_end = end_datetime or dt_util.as_utc( + dt_util.start_of_local_day(end_date + timedelta(days=1)) + ) + prices = __filter_prices( + prices, + data.intervals, + filter_start, + filter_end, + ) - if price_type == PriceType.ENERGY_USAGE: - return __serialize_prices(data.timestamp_usage_prices) - return __serialize_prices(data.timestamp_return_prices) + return __serialize_prices(prices) @callback @@ -150,6 +198,6 @@ def async_setup_services(hass: HomeAssistant) -> None: DOMAIN, ENERGY_RETURN_SERVICE_NAME, partial(__get_prices, price_type=PriceType.ENERGY_RETURN), - schema=SERVICE_SCHEMA, + schema=RETURN_SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) diff --git a/requirements_all.txt b/requirements_all.txt index e86cfeac3f4da..abc793907041d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -859,7 +859,7 @@ eagle100==0.1.1 earn-e-p1==0.1.0 # homeassistant.components.easyenergy -easyenergy==2.2.0 +easyenergy==3.0.0 # homeassistant.components.ebusd ebusdpy==0.0.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af20fdc51dbf1..e43ec3edac02c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -768,7 +768,7 @@ eagle100==0.1.1 earn-e-p1==0.1.0 # homeassistant.components.easyenergy -easyenergy==2.2.0 +easyenergy==3.0.0 # homeassistant.components.egauge egauge-async==0.4.0 diff --git a/tests/components/easyenergy/conftest.py b/tests/components/easyenergy/conftest.py index f2ed2cf4dbc53..4aa1f76e1ed08 100644 --- a/tests/components/easyenergy/conftest.py +++ b/tests/components/easyenergy/conftest.py @@ -9,7 +9,7 @@ from homeassistant.components.easyenergy.const import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, async_load_json_array_fixture +from tests.common import MockConfigEntry, async_load_json_object_fixture @pytest.fixture @@ -39,11 +39,18 @@ async def mock_easyenergy(hass: HomeAssistant) -> AsyncGenerator[MagicMock]: "homeassistant.components.easyenergy.coordinator.EasyEnergy", autospec=True ) as easyenergy_mock: client = easyenergy_mock.return_value + energy_data = await async_load_json_object_fixture( + hass, "today_energy.json", DOMAIN + ) client.energy_prices.return_value = Electricity.from_dict( - await async_load_json_array_fixture(hass, "today_energy.json", DOMAIN) + energy_data["prices"], + price_key="priceIncVat", + return_price_key="priceIncVat", ) + gas_data = await async_load_json_object_fixture(hass, "today_gas.json", DOMAIN) client.gas_prices.return_value = Gas.from_dict( - await async_load_json_array_fixture(hass, "today_gas.json", DOMAIN) + gas_data["prices"], + price_key="priceIncVat", ) yield client diff --git a/tests/components/easyenergy/fixtures/today_energy.json b/tests/components/easyenergy/fixtures/today_energy.json index 8e91a6244ac43..79725f9f5c7cf 100644 --- a/tests/components/easyenergy/fixtures/today_energy.json +++ b/tests/components/easyenergy/fixtures/today_energy.json @@ -1,146 +1,317 @@ -[ - { - "Timestamp": "2023-01-18T23:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1349513, - "TariffReturn": 0.11153 - }, - { - "Timestamp": "2023-01-19T00:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1294458, - "TariffReturn": 0.10698 - }, - { - "Timestamp": "2023-01-19T01:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1270137, - "TariffReturn": 0.10497 - }, - { - "Timestamp": "2023-01-19T02:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1230812, - "TariffReturn": 0.10172 - }, - { - "Timestamp": "2023-01-19T03:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1297483, - "TariffReturn": 0.10723 - }, - { - "Timestamp": "2023-01-19T04:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1386902, - "TariffReturn": 0.11462 - }, - { - "Timestamp": "2023-01-19T05:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1439174, - "TariffReturn": 0.11894 - }, - { - "Timestamp": "2023-01-19T06:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.193479, - "TariffReturn": 0.1599 - }, - { - "Timestamp": "2023-01-19T07:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.19844, - "TariffReturn": 0.164 - }, - { - "Timestamp": "2023-01-19T08:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.2077449, - "TariffReturn": 0.17169 - }, - { - "Timestamp": "2023-01-19T09:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.16819, - "TariffReturn": 0.139 - }, - { - "Timestamp": "2023-01-19T10:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1649835, - "TariffReturn": 0.13635 - }, - { - "Timestamp": "2023-01-19T11:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.156816, - "TariffReturn": 0.1296 - }, - { - "Timestamp": "2023-01-19T12:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1873927, - "TariffReturn": 0.15487 - }, - { - "Timestamp": "2023-01-19T13:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1941929, - "TariffReturn": 0.16049 - }, - { - "Timestamp": "2023-01-19T14:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.2129116, - "TariffReturn": 0.17596 - }, - { - "Timestamp": "2023-01-19T15:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.2254109, - "TariffReturn": 0.18629 - }, - { - "Timestamp": "2023-01-19T16:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.2467674, - "TariffReturn": 0.20394 - }, - { - "Timestamp": "2023-01-19T17:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.2390597, - "TariffReturn": 0.19757 - }, - { - "Timestamp": "2023-01-19T18:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.2074303, - "TariffReturn": 0.17143 - }, - { - "Timestamp": "2023-01-19T19:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1815, - "TariffReturn": 0.15 - }, - { - "Timestamp": "2023-01-19T20:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1795761, - "TariffReturn": 0.14841 - }, - { - "Timestamp": "2023-01-19T21:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.1807014, - "TariffReturn": 0.14934 - }, - { - "Timestamp": "2023-01-19T22:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.16819, - "TariffReturn": 0.139 - } -] +{ + "providedAt": "2026-04-19T22:16:44.4866876Z", + "prices": [ + { + "from": "2026-04-19T00:00:00.0000000", + "until": "2026-04-19T01:00:00.0000000", + "price": 0.11549, + "unit": "kWh", + "priceIncVat": 0.13975, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.27238, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T01:00:00.0000000", + "until": "2026-04-19T02:00:00.0000000", + "price": 0.10522, + "unit": "kWh", + "priceIncVat": 0.12732, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.25995, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T02:00:00.0000000", + "until": "2026-04-19T03:00:00.0000000", + "price": 0.1092, + "unit": "kWh", + "priceIncVat": 0.13213, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.26476, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T03:00:00.0000000", + "until": "2026-04-19T04:00:00.0000000", + "price": 0.10325, + "unit": "kWh", + "priceIncVat": 0.12493, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.25756, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T04:00:00.0000000", + "until": "2026-04-19T05:00:00.0000000", + "price": 0.10346, + "unit": "kWh", + "priceIncVat": 0.12519, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.25782, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T05:00:00.0000000", + "until": "2026-04-19T06:00:00.0000000", + "price": 0.09795, + "unit": "kWh", + "priceIncVat": 0.11852, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.25115, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T06:00:00.0000000", + "until": "2026-04-19T07:00:00.0000000", + "price": 0.10071, + "unit": "kWh", + "priceIncVat": 0.12186, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.25449, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T07:00:00.0000000", + "until": "2026-04-19T08:00:00.0000000", + "price": 0.09793, + "unit": "kWh", + "priceIncVat": 0.1185, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.25113, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T08:00:00.0000000", + "until": "2026-04-19T09:00:00.0000000", + "price": 0.09725, + "unit": "kWh", + "priceIncVat": 0.11768, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.25031, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T09:00:00.0000000", + "until": "2026-04-19T10:00:00.0000000", + "price": 0.08766, + "unit": "kWh", + "priceIncVat": 0.10607, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.2387, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T10:00:00.0000000", + "until": "2026-04-19T11:00:00.0000000", + "price": 0.06132, + "unit": "kWh", + "priceIncVat": 0.07419, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.20682, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T11:00:00.0000000", + "until": "2026-04-19T12:00:00.0000000", + "price": 0.02832, + "unit": "kWh", + "priceIncVat": 0.03426, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.16689, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T12:00:00.0000000", + "until": "2026-04-19T13:00:00.0000000", + "price": 0.00806, + "unit": "kWh", + "priceIncVat": 0.00975, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.14238, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T13:00:00.0000000", + "until": "2026-04-19T14:00:00.0000000", + "price": -0.00085, + "unit": "kWh", + "priceIncVat": -0.00085, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.13178, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T14:00:00.0000000", + "until": "2026-04-19T15:00:00.0000000", + "price": -0.003, + "unit": "kWh", + "priceIncVat": -0.003, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.12963, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T15:00:00.0000000", + "until": "2026-04-19T16:00:00.0000000", + "price": -0.00226, + "unit": "kWh", + "priceIncVat": -0.00226, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.13037, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T16:00:00.0000000", + "until": "2026-04-19T17:00:00.0000000", + "price": 0.01348, + "unit": "kWh", + "priceIncVat": 0.01631, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.14894, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T17:00:00.0000000", + "until": "2026-04-19T18:00:00.0000000", + "price": 0.06533, + "unit": "kWh", + "priceIncVat": 0.07905, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.21168, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T18:00:00.0000000", + "until": "2026-04-19T19:00:00.0000000", + "price": 0.10385, + "unit": "kWh", + "priceIncVat": 0.12566, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.25829, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T19:00:00.0000000", + "until": "2026-04-19T20:00:00.0000000", + "price": 0.11681, + "unit": "kWh", + "priceIncVat": 0.14134, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.27397, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T20:00:00.0000000", + "until": "2026-04-19T21:00:00.0000000", + "price": 0.12464, + "unit": "kWh", + "priceIncVat": 0.15082, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.28345, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T21:00:00.0000000", + "until": "2026-04-19T22:00:00.0000000", + "price": 0.12214, + "unit": "kWh", + "priceIncVat": 0.14779, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.28042, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T22:00:00.0000000", + "until": "2026-04-19T23:00:00.0000000", + "price": 0.11906, + "unit": "kWh", + "priceIncVat": 0.14407, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.2767, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + }, + { + "from": "2026-04-19T23:00:00.0000000", + "until": "2026-04-20T00:00:00.0000000", + "price": 0.1113, + "unit": "kWh", + "priceIncVat": 0.13467, + "energyTax": 0.11085, + "purchasePrice": 0.02178, + "invoicePrice": 0.2673, + "average": 0.0786, + "averageInc": 0.09516, + "granularity": "hour" + } + ] +} diff --git a/tests/components/easyenergy/fixtures/today_gas.json b/tests/components/easyenergy/fixtures/today_gas.json index ed3e0106b06c3..b51b25d7dbffc 100644 --- a/tests/components/easyenergy/fixtures/today_gas.json +++ b/tests/components/easyenergy/fixtures/today_gas.json @@ -1,146 +1,31 @@ -[ - { - "Timestamp": "2023-01-19T05:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T06:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T07:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T08:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T09:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T10:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T11:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T12:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T13:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T14:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T15:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T16:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T17:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T18:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T19:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T20:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T21:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T22:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-19T23:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-20T00:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-20T01:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-20T02:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-20T03:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - }, - { - "Timestamp": "2023-01-20T04:00:00+00:00", - "SupplierId": 0, - "TariffUsage": 0.7252982, - "TariffReturn": 0.7252982 - } -] +{ + "providedAt": "2026-04-19T22:16:43.4740091Z", + "prices": [ + { + "from": "2026-04-19T00:00:00.0000000", + "until": "2026-04-20T00:00:00.0000000", + "price": 0.50983, + "unit": "m3", + "priceIncVat": 0.6169, + "energyTax": 0.7268, + "purchasePrice": 0.079, + "invoicePrice": 1.4227, + "average": 0.49402, + "averageInc": 0.59776, + "granularity": "day" + }, + { + "from": "2026-04-20T00:00:00.0000000", + "until": "2026-04-21T00:00:00.0000000", + "price": 0.4782, + "unit": "m3", + "priceIncVat": 0.57862, + "energyTax": 0.7268, + "purchasePrice": 0.079, + "invoicePrice": 1.38442, + "average": 0.49402, + "averageInc": 0.59776, + "granularity": "day" + } + ] +} diff --git a/tests/components/easyenergy/snapshots/test_diagnostics.ambr b/tests/components/easyenergy/snapshots/test_diagnostics.ambr index 805846832aa7f..50bc963d9e4f3 100644 --- a/tests/components/easyenergy/snapshots/test_diagnostics.ambr +++ b/tests/components/easyenergy/snapshots/test_diagnostics.ambr @@ -2,55 +2,55 @@ # name: test_diagnostics dict({ 'energy_return': dict({ - 'average_price': 0.14599, - 'current_hour_price': 0.18629, - 'highest_price_time': '2023-01-19T16:00:00+00:00', - 'lowest_price_time': '2023-01-19T02:00:00+00:00', - 'max_price': 0.20394, - 'min_price': 0.10172, - 'next_hour_price': 0.20394, - 'percentage_of_max': 91.35, + 'average_price': 0.09516, + 'current_hour_price': -0.00226, + 'highest_price_time': '2026-04-19T18:00:00+00:00', + 'lowest_price_time': '2026-04-19T12:00:00+00:00', + 'max_price': 0.15082, + 'min_price': -0.003, + 'next_hour_price': 0.01631, + 'percentage_of_max': -1.5, }), 'energy_usage': dict({ - 'average_price': 0.17665, - 'current_hour_price': 0.22541, - 'highest_price_time': '2023-01-19T16:00:00+00:00', - 'lowest_price_time': '2023-01-19T02:00:00+00:00', - 'max_price': 0.24677, - 'min_price': 0.12308, - 'next_hour_price': 0.24677, - 'percentage_of_max': 91.34, + 'average_price': 0.09516, + 'current_hour_price': -0.00226, + 'highest_price_time': '2026-04-19T18:00:00+00:00', + 'lowest_price_time': '2026-04-19T12:00:00+00:00', + 'max_price': 0.15082, + 'min_price': -0.003, + 'next_hour_price': 0.01631, + 'percentage_of_max': -1.5, }), 'entry': dict({ 'title': 'energy', }), 'gas': dict({ - 'current_hour_price': 0.7253, - 'next_hour_price': 0.7253, + 'current_hour_price': 0.6169, + 'next_hour_price': 0.6169, }), }) # --- # name: test_diagnostics_no_gas_today dict({ 'energy_return': dict({ - 'average_price': 0.14599, - 'current_hour_price': 0.18629, - 'highest_price_time': '2023-01-19T16:00:00+00:00', - 'lowest_price_time': '2023-01-19T02:00:00+00:00', - 'max_price': 0.20394, - 'min_price': 0.10172, - 'next_hour_price': 0.20394, - 'percentage_of_max': 91.35, + 'average_price': 0.09516, + 'current_hour_price': -0.00226, + 'highest_price_time': '2026-04-19T18:00:00+00:00', + 'lowest_price_time': '2026-04-19T12:00:00+00:00', + 'max_price': 0.15082, + 'min_price': -0.003, + 'next_hour_price': 0.01631, + 'percentage_of_max': -1.5, }), 'energy_usage': dict({ - 'average_price': 0.17665, - 'current_hour_price': 0.22541, - 'highest_price_time': '2023-01-19T16:00:00+00:00', - 'lowest_price_time': '2023-01-19T02:00:00+00:00', - 'max_price': 0.24677, - 'min_price': 0.12308, - 'next_hour_price': 0.24677, - 'percentage_of_max': 91.34, + 'average_price': 0.09516, + 'current_hour_price': -0.00226, + 'highest_price_time': '2026-04-19T18:00:00+00:00', + 'lowest_price_time': '2026-04-19T12:00:00+00:00', + 'max_price': 0.15082, + 'min_price': -0.003, + 'next_hour_price': 0.01631, + 'percentage_of_max': -1.5, }), 'entry': dict({ 'title': 'energy', diff --git a/tests/components/easyenergy/snapshots/test_services.ambr b/tests/components/easyenergy/snapshots/test_services.ambr index 3330e5cf03c0c..4c197e66cf243 100644 --- a/tests/components/easyenergy/snapshots/test_services.ambr +++ b/tests/components/easyenergy/snapshots/test_services.ambr @@ -3,100 +3,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.11153, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.10698, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.10497, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.10172, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.10723, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.11462, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.11894, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.1599, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.164, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.17169, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.13635, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.1296, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.15487, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.16049, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.17596, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.18629, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.20394, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.19757, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.17143, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.15, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.14841, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.14934, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -105,100 +105,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.13495, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.12945, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.12701, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.12308, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.12975, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.13869, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.14392, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.19348, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.19844, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.20774, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.16498, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.15682, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.18739, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.19419, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.21291, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.22541, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.24677, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.23906, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.20743, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.1815, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.17958, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.1807, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -207,100 +207,12 @@ dict({ 'prices': list([ dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.6169, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 06:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 07:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 08:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 09:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 10:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 11:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 12:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 13:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 14:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 15:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 16:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 17:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 18:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 19:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 20:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 21:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 22:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 23:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 00:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 01:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 02:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 03:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 04:00:00+00:00', + 'price': 0.57862, + 'timestamp': '2026-04-19 22:00:00+00:00', }), ]), }) @@ -309,100 +221,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.11153, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.10698, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.10497, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.10172, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.10723, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.11462, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.11894, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.1599, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.164, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.17169, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.13635, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.1296, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.15487, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.16049, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.17596, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.18629, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.20394, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.19757, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.17143, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.15, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.14841, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.14934, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -411,100 +323,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.13495, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.12945, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.12701, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.12308, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.12975, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.13869, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.14392, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.19348, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.19844, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.20774, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.16498, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.15682, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.18739, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.19419, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.21291, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.22541, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.24677, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.23906, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.20743, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.1815, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.17958, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.1807, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -513,100 +425,12 @@ dict({ 'prices': list([ dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 05:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 06:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 07:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 08:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 09:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 10:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 11:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 12:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 13:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 14:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 15:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 16:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 17:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 18:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 19:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 20:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 21:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 22:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 23:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 00:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 01:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 02:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 03:00:00+00:00', + 'price': 0.6169, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 04:00:00+00:00', + 'price': 0.57862, + 'timestamp': '2026-04-19 22:00:00+00:00', }), ]), }) @@ -615,100 +439,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.11153, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.10698, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.10497, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.10172, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.10723, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.11462, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.11894, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.1599, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.164, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.17169, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.13635, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.1296, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.15487, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.16049, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.17596, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.18629, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.20394, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.19757, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.17143, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.15, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.14841, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.14934, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -717,100 +541,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.13495, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.12945, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.12701, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.12308, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.12975, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.13869, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.14392, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.19348, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.19844, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.20774, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.16498, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.15682, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.18739, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.19419, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.21291, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.22541, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.24677, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.23906, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.20743, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.1815, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.17958, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.1807, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -819,100 +643,12 @@ dict({ 'prices': list([ dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.6169, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 06:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 07:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 08:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 09:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 10:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 11:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 12:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 13:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 14:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 15:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 16:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 17:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 18:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 19:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 20:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 21:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 22:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 23:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 00:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 01:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 02:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 03:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 04:00:00+00:00', + 'price': 0.57862, + 'timestamp': '2026-04-19 22:00:00+00:00', }), ]), }) @@ -921,100 +657,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.11153, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.10698, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.10497, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.10172, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.10723, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.11462, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.11894, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.1599, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.164, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.17169, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.13635, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.1296, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.15487, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.16049, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.17596, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.18629, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.20394, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.19757, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.17143, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.15, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.14841, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.14934, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -1023,100 +759,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.13495, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.12945, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.12701, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.12308, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.12975, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.13869, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.14392, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.19348, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.19844, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.20774, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.16498, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.15682, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.18739, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.19419, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.21291, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.22541, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.24677, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.23906, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.20743, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.1815, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.17958, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.1807, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -1125,100 +861,12 @@ dict({ 'prices': list([ dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 05:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 06:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 07:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 08:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 09:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 10:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 11:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 12:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 13:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 14:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 15:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 16:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 17:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 18:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 19:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 20:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 21:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 22:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 23:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 00:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 01:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 02:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 03:00:00+00:00', + 'price': 0.6169, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 04:00:00+00:00', + 'price': 0.57862, + 'timestamp': '2026-04-19 22:00:00+00:00', }), ]), }) @@ -1227,100 +875,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.11153, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.10698, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.10497, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.10172, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.10723, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.11462, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.11894, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.1599, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.164, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.17169, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.13635, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.1296, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.15487, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.16049, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.17596, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.18629, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.20394, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.19757, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.17143, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.15, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.14841, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.14934, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -1329,100 +977,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.13495, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.12945, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.12701, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.12308, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.12975, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.13869, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.14392, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.19348, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.19844, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.20774, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.16498, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.15682, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.18739, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.19419, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.21291, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.22541, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.24677, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.23906, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.20743, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.1815, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.17958, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.1807, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -1431,100 +1079,12 @@ dict({ 'prices': list([ dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.6169, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 06:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 07:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 08:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 09:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 10:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 11:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 12:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 13:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 14:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 15:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 16:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 17:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 18:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 19:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 20:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 21:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 22:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 23:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 00:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 01:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 02:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 03:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 04:00:00+00:00', + 'price': 0.57862, + 'timestamp': '2026-04-19 22:00:00+00:00', }), ]), }) @@ -1533,100 +1093,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.11153, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.10698, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.10497, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.10172, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.10723, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.11462, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.11894, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.1599, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.164, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.17169, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.13635, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.1296, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.15487, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.16049, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.17596, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.18629, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.20394, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.19757, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.17143, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.15, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.14841, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.14934, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -1635,100 +1195,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.13495, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.12945, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.12701, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.12308, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.12975, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.13869, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.14392, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.19348, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.19844, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.20774, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.16498, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.15682, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.18739, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.19419, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.21291, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.22541, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.24677, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.23906, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.20743, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.1815, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.17958, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.1807, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -1737,100 +1297,12 @@ dict({ 'prices': list([ dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 05:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 06:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 07:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 08:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 09:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 10:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 11:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 12:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 13:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 14:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 15:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 16:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 17:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 18:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 19:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 20:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 21:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 22:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 23:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 00:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 01:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 02:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 03:00:00+00:00', + 'price': 0.6169, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 04:00:00+00:00', + 'price': 0.57862, + 'timestamp': '2026-04-19 22:00:00+00:00', }), ]), }) @@ -1839,100 +1311,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.11153, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.10698, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.10497, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.10172, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.10723, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.11462, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.11894, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.1599, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.164, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.17169, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.13635, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.1296, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.15487, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.16049, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.17596, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.18629, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.20394, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.19757, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.17143, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.15, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.14841, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.14934, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -1941,100 +1413,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.13495, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.12945, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.12701, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.12308, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.12975, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.13869, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.14392, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.19348, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.19844, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.20774, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.16498, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.15682, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.18739, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.19419, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.21291, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.22541, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.24677, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.23906, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.20743, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.1815, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.17958, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.1807, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -2043,100 +1515,12 @@ dict({ 'prices': list([ dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.6169, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 06:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 07:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 08:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 09:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 10:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 11:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 12:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 13:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 14:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 15:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 16:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 17:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 18:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 19:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 20:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 21:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 22:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 23:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 00:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 01:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 02:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 03:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 04:00:00+00:00', + 'price': 0.57862, + 'timestamp': '2026-04-19 22:00:00+00:00', }), ]), }) @@ -2145,100 +1529,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.11153, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.10698, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.10497, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.10172, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.10723, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.11462, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.11894, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.1599, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.164, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.17169, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.13635, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.1296, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.15487, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.16049, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.17596, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.18629, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.20394, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.19757, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.17143, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.15, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.14841, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.14934, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.139, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -2247,100 +1631,100 @@ dict({ 'prices': list([ dict({ - 'price': 0.13495, - 'timestamp': '2023-01-18 23:00:00+00:00', + 'price': 0.13975, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.12945, - 'timestamp': '2023-01-19 00:00:00+00:00', + 'price': 0.12732, + 'timestamp': '2026-04-18 23:00:00+00:00', }), dict({ - 'price': 0.12701, - 'timestamp': '2023-01-19 01:00:00+00:00', + 'price': 0.13213, + 'timestamp': '2026-04-19 00:00:00+00:00', }), dict({ - 'price': 0.12308, - 'timestamp': '2023-01-19 02:00:00+00:00', + 'price': 0.12493, + 'timestamp': '2026-04-19 01:00:00+00:00', }), dict({ - 'price': 0.12975, - 'timestamp': '2023-01-19 03:00:00+00:00', + 'price': 0.12519, + 'timestamp': '2026-04-19 02:00:00+00:00', }), dict({ - 'price': 0.13869, - 'timestamp': '2023-01-19 04:00:00+00:00', + 'price': 0.11852, + 'timestamp': '2026-04-19 03:00:00+00:00', }), dict({ - 'price': 0.14392, - 'timestamp': '2023-01-19 05:00:00+00:00', + 'price': 0.12186, + 'timestamp': '2026-04-19 04:00:00+00:00', }), dict({ - 'price': 0.19348, - 'timestamp': '2023-01-19 06:00:00+00:00', + 'price': 0.1185, + 'timestamp': '2026-04-19 05:00:00+00:00', }), dict({ - 'price': 0.19844, - 'timestamp': '2023-01-19 07:00:00+00:00', + 'price': 0.11768, + 'timestamp': '2026-04-19 06:00:00+00:00', }), dict({ - 'price': 0.20774, - 'timestamp': '2023-01-19 08:00:00+00:00', + 'price': 0.10607, + 'timestamp': '2026-04-19 07:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 09:00:00+00:00', + 'price': 0.07419, + 'timestamp': '2026-04-19 08:00:00+00:00', }), dict({ - 'price': 0.16498, - 'timestamp': '2023-01-19 10:00:00+00:00', + 'price': 0.03426, + 'timestamp': '2026-04-19 09:00:00+00:00', }), dict({ - 'price': 0.15682, - 'timestamp': '2023-01-19 11:00:00+00:00', + 'price': 0.00975, + 'timestamp': '2026-04-19 10:00:00+00:00', }), dict({ - 'price': 0.18739, - 'timestamp': '2023-01-19 12:00:00+00:00', + 'price': -0.00085, + 'timestamp': '2026-04-19 11:00:00+00:00', }), dict({ - 'price': 0.19419, - 'timestamp': '2023-01-19 13:00:00+00:00', + 'price': -0.003, + 'timestamp': '2026-04-19 12:00:00+00:00', }), dict({ - 'price': 0.21291, - 'timestamp': '2023-01-19 14:00:00+00:00', + 'price': -0.00226, + 'timestamp': '2026-04-19 13:00:00+00:00', }), dict({ - 'price': 0.22541, - 'timestamp': '2023-01-19 15:00:00+00:00', + 'price': 0.01631, + 'timestamp': '2026-04-19 14:00:00+00:00', }), dict({ - 'price': 0.24677, - 'timestamp': '2023-01-19 16:00:00+00:00', + 'price': 0.07905, + 'timestamp': '2026-04-19 15:00:00+00:00', }), dict({ - 'price': 0.23906, - 'timestamp': '2023-01-19 17:00:00+00:00', + 'price': 0.12566, + 'timestamp': '2026-04-19 16:00:00+00:00', }), dict({ - 'price': 0.20743, - 'timestamp': '2023-01-19 18:00:00+00:00', + 'price': 0.14134, + 'timestamp': '2026-04-19 17:00:00+00:00', }), dict({ - 'price': 0.1815, - 'timestamp': '2023-01-19 19:00:00+00:00', + 'price': 0.15082, + 'timestamp': '2026-04-19 18:00:00+00:00', }), dict({ - 'price': 0.17958, - 'timestamp': '2023-01-19 20:00:00+00:00', + 'price': 0.14779, + 'timestamp': '2026-04-19 19:00:00+00:00', }), dict({ - 'price': 0.1807, - 'timestamp': '2023-01-19 21:00:00+00:00', + 'price': 0.14407, + 'timestamp': '2026-04-19 20:00:00+00:00', }), dict({ - 'price': 0.16819, - 'timestamp': '2023-01-19 22:00:00+00:00', + 'price': 0.13467, + 'timestamp': '2026-04-19 21:00:00+00:00', }), ]), }) @@ -2349,100 +1733,12 @@ dict({ 'prices': list([ dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 05:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 06:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 07:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 08:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 09:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 10:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 11:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 12:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 13:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 14:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 15:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 16:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 17:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 18:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 19:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 20:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 21:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 22:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-19 23:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 00:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 01:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 02:00:00+00:00', - }), - dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 03:00:00+00:00', + 'price': 0.6169, + 'timestamp': '2026-04-18 22:00:00+00:00', }), dict({ - 'price': 0.7253, - 'timestamp': '2023-01-20 04:00:00+00:00', + 'price': 0.57862, + 'timestamp': '2026-04-19 22:00:00+00:00', }), ]), }) diff --git a/tests/components/easyenergy/test_diagnostics.py b/tests/components/easyenergy/test_diagnostics.py index 3820078a00bd8..f493487e44dce 100644 --- a/tests/components/easyenergy/test_diagnostics.py +++ b/tests/components/easyenergy/test_diagnostics.py @@ -19,7 +19,7 @@ from tests.typing import ClientSessionGenerator -@pytest.mark.freeze_time("2023-01-19 15:00:00") +@pytest.mark.freeze_time("2026-04-19 13:00:00+00:00") async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -33,7 +33,7 @@ async def test_diagnostics( ) -@pytest.mark.freeze_time("2023-01-19 15:00:00") +@pytest.mark.freeze_time("2026-04-19 13:00:00+00:00") async def test_diagnostics_no_gas_today( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/easyenergy/test_sensor.py b/tests/components/easyenergy/test_sensor.py index 5ca4740dc6f80..268b3d2b6f68f 100644 --- a/tests/components/easyenergy/test_sensor.py +++ b/tests/components/easyenergy/test_sensor.py @@ -33,7 +33,7 @@ from tests.common import MockConfigEntry -@pytest.mark.freeze_time("2023-01-19 15:00:00") +@pytest.mark.freeze_time("2026-04-19 13:00:00+00:00") async def test_energy_usage_today( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -51,7 +51,7 @@ async def test_energy_usage_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_energy_usage_current_hour_price" - assert state.state == "0.22541" + assert state.state == "-0.00226" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Usage Current hour" @@ -72,7 +72,7 @@ async def test_energy_usage_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_energy_usage_average_price" - assert state.state == "0.17665" + assert state.state == "0.09516" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Usage Average - today" @@ -90,7 +90,7 @@ async def test_energy_usage_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_energy_usage_max_price" - assert state.state == "0.24677" + assert state.state == "0.15082" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Usage Highest price - today" @@ -110,7 +110,7 @@ async def test_energy_usage_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_energy_usage_highest_price_time" - assert state.state == "2023-01-19T16:00:00+00:00" + assert state.state == "2026-04-19T18:00:00+00:00" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Usage Time of highest price - today" @@ -140,7 +140,7 @@ async def test_energy_usage_today( assert ( entry.unique_id == f"{entry_id}_today_energy_usage_hours_priced_equal_or_lower" ) - assert state.state == "21" + assert state.state == "2" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Usage Hours priced equal or lower than current - today" @@ -148,7 +148,7 @@ async def test_energy_usage_today( assert ATTR_DEVICE_CLASS not in state.attributes -@pytest.mark.freeze_time("2023-01-19 15:00:00") +@pytest.mark.freeze_time("2026-04-19 13:00:00+00:00") async def test_energy_return_today( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -166,7 +166,7 @@ async def test_energy_return_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_energy_return_current_hour_price" - assert state.state == "0.18629" + assert state.state == "-0.00226" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Return Current hour" @@ -187,7 +187,7 @@ async def test_energy_return_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_energy_return_average_price" - assert state.state == "0.14599" + assert state.state == "0.09516" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Return Average - today" @@ -205,7 +205,7 @@ async def test_energy_return_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_energy_return_max_price" - assert state.state == "0.20394" + assert state.state == "0.15082" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Return Highest price - today" @@ -225,7 +225,7 @@ async def test_energy_return_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_energy_return_highest_price_time" - assert state.state == "2023-01-19T16:00:00+00:00" + assert state.state == "2026-04-19T18:00:00+00:00" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Return Time of highest price - today" @@ -256,7 +256,7 @@ async def test_energy_return_today( entry.unique_id == f"{entry_id}_today_energy_return_hours_priced_equal_or_higher" ) - assert state.state == "3" + assert state.state == "23" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price - Return Hours priced equal or higher than current - today" @@ -264,7 +264,7 @@ async def test_energy_return_today( assert ATTR_DEVICE_CLASS not in state.attributes -@pytest.mark.freeze_time("2023-01-19 10:00:00") +@pytest.mark.freeze_time("2026-04-19 10:00:00+00:00") async def test_gas_today( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -280,7 +280,7 @@ async def test_gas_today( assert entry assert state assert entry.unique_id == f"{entry_id}_today_gas_current_hour_price" - assert state.state == "0.7253" + assert state.state == "0.6169" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Gas market price Current hour" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -301,7 +301,7 @@ async def test_gas_today( assert not device_entry.sw_version -@pytest.mark.freeze_time("2023-01-19 15:00:00") +@pytest.mark.freeze_time("2026-04-19 13:00:00+00:00") async def test_no_gas_today( hass: HomeAssistant, mock_easyenergy: MagicMock, init_integration: MockConfigEntry ) -> None: diff --git a/tests/components/easyenergy/test_services.py b/tests/components/easyenergy/test_services.py index aaaf9b6d9694e..4df513a77f28b 100644 --- a/tests/components/easyenergy/test_services.py +++ b/tests/components/easyenergy/test_services.py @@ -1,5 +1,9 @@ """Tests for the services provided by the easyEnergy integration.""" +from datetime import date +from unittest.mock import MagicMock + +from easyenergy import VatOption import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol @@ -37,8 +41,8 @@ async def test_has_services( ], ) @pytest.mark.parametrize("incl_vat", [{"incl_vat": False}, {"incl_vat": True}]) -@pytest.mark.parametrize("start", [{"start": "2023-01-01 00:00:00"}, {}]) -@pytest.mark.parametrize("end", [{"end": "2023-01-01 00:00:00"}, {}]) +@pytest.mark.parametrize("start", [{"start": "2023-01-01"}, {}]) +@pytest.mark.parametrize("end", [{"end": "2023-01-01"}, {}]) async def test_service( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -48,8 +52,10 @@ async def test_service( start: dict[str, str], end: dict[str, str], ) -> None: - """Test the EnergyZero Service.""" + """Test the easyEnergy service.""" entry = {ATTR_CONFIG_ENTRY: mock_config_entry.entry_id} + if service == ENERGY_RETURN_SERVICE_NAME: + incl_vat = {} data = entry | incl_vat | start | end @@ -62,6 +68,75 @@ async def test_service( ) +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize( + ("service", "expected_prices"), + [ + ( + GAS_SERVICE_NAME, + [ + {"timestamp": "2026-04-18 22:00:00+00:00", "price": 0.6169}, + ], + ), + ( + ENERGY_USAGE_SERVICE_NAME, + [ + {"timestamp": "2026-04-19 00:00:00+00:00", "price": 0.13213}, + {"timestamp": "2026-04-19 01:00:00+00:00", "price": 0.12493}, + {"timestamp": "2026-04-19 02:00:00+00:00", "price": 0.12519}, + ], + ), + ( + ENERGY_RETURN_SERVICE_NAME, + [ + {"timestamp": "2026-04-19 00:00:00+00:00", "price": 0.13213}, + {"timestamp": "2026-04-19 01:00:00+00:00", "price": 0.12493}, + {"timestamp": "2026-04-19 02:00:00+00:00", "price": 0.12519}, + ], + ), + ], +) +async def test_service_filters_datetime_range( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_easyenergy: MagicMock, + service: str, + expected_prices: list[dict[str, str | float]], +) -> None: + """Test easyEnergy services filter returned day data to a datetime range.""" + service_data: dict[str, str | bool] = { + ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, + "start": "2026-04-19 02:00:00+02:00", + "end": "2026-04-19 05:00:00+02:00", + } + if service != ENERGY_RETURN_SERVICE_NAME: + service_data["incl_vat"] = True + + mock_easyenergy.reset_mock() + + response = await hass.services.async_call( + DOMAIN, + service, + service_data, + blocking=True, + return_response=True, + ) + + assert response == {"prices": expected_prices} + + expected_call = { + "start_date": date(2026, 4, 19), + "end_date": date(2026, 4, 19), + "vat": VatOption.INCLUDE, + } + if service == GAS_SERVICE_NAME: + mock_easyenergy.gas_prices.assert_called_once_with(**expected_call) + mock_easyenergy.energy_prices.assert_not_called() + else: + mock_easyenergy.energy_prices.assert_called_once_with(**expected_call) + mock_easyenergy.gas_prices.assert_not_called() + + @pytest.fixture def config_entry_data( mock_config_entry: MockConfigEntry, request: pytest.FixtureRequest @@ -83,65 +158,22 @@ def config_entry_data( ], ) @pytest.mark.parametrize( - ("config_entry_data", "service_data", "error", "error_message"), + ("config_entry_data", "service_data", "error_message"), [ - ({}, {}, vol.er.Error, "required key not provided .+"), - ( - {"config_entry": True}, - {}, - vol.er.Error, - "required key not provided .+", - ), - ( - {}, - {"incl_vat": True}, - vol.er.Error, - "required key not provided .+", - ), - ( - {"config_entry": True}, - {"incl_vat": "incorrect vat"}, - vol.er.Error, - "expected bool for dictionary value .+", - ), - ( - {"config_entry": "incorrect entry"}, - {"incl_vat": True}, - ServiceValidationError, - "config entry with ID incorrect entry was not found", - ), - ( - {"config_entry": True}, - { - "incl_vat": True, - "start": "incorrect date", - }, - ServiceValidationError, - "Invalid datetime provided.", - ), - ( - {"config_entry": True}, - { - "incl_vat": True, - "end": "incorrect date", - }, - ServiceValidationError, - "Invalid datetime provided.", - ), + ({}, {}, "required key not provided .+"), ], indirect=["config_entry_data"], ) -async def test_service_validation( +async def test_service_schema_validation( hass: HomeAssistant, service: str, config_entry_data: dict[str, str], service_data: dict[str, str | bool], - error: type[Exception], error_message: str, ) -> None: - """Test the easyEnergy Service.""" + """Test easyEnergy service schema validation.""" - with pytest.raises(error, match=error_message): + with pytest.raises(vol.er.Error, match=error_message): await hass.services.async_call( DOMAIN, service, @@ -149,3 +181,122 @@ async def test_service_validation( blocking=True, return_response=True, ) + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize("service", [GAS_SERVICE_NAME, ENERGY_USAGE_SERVICE_NAME]) +@pytest.mark.parametrize( + ("service_data", "error_message"), + [ + ({}, "required key not provided .+"), + ({"incl_vat": "incorrect vat"}, "expected bool for dictionary value .+"), + ], +) +async def test_service_schema_validation_vat( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + service: str, + service_data: dict[str, str | bool], + error_message: str, +) -> None: + """Test easyEnergy service schema validation for VAT.""" + + with pytest.raises(vol.er.Error, match=error_message): + await hass.services.async_call( + DOMAIN, + service, + {ATTR_CONFIG_ENTRY: mock_config_entry.entry_id} | service_data, + blocking=True, + return_response=True, + ) + + +@pytest.mark.usefixtures("init_integration") +async def test_service_schema_validation_return_vat( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test return prices do not accept VAT selection.""" + + with pytest.raises(vol.er.Error, match="extra keys not allowed .+"): + await hass.services.async_call( + DOMAIN, + ENERGY_RETURN_SERVICE_NAME, + {ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, "incl_vat": True}, + blocking=True, + return_response=True, + ) + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize( + "service", + [ + GAS_SERVICE_NAME, + ENERGY_USAGE_SERVICE_NAME, + ENERGY_RETURN_SERVICE_NAME, + ], +) +async def test_service_validation_config_entry_not_found( + hass: HomeAssistant, + service: str, +) -> None: + """Test config entry validation for easyEnergy services.""" + service_data: dict[str, str | bool] = {ATTR_CONFIG_ENTRY: "incorrect entry"} + if service != ENERGY_RETURN_SERVICE_NAME: + service_data["incl_vat"] = True + + with pytest.raises(ServiceValidationError) as err: + await hass.services.async_call( + DOMAIN, + service, + service_data, + blocking=True, + return_response=True, + ) + + assert err.value.translation_key == "service_config_entry_not_found" + assert err.value.translation_placeholders == { + "domain": DOMAIN, + "entry_id": "incorrect entry", + } + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize( + "service", + [ + GAS_SERVICE_NAME, + ENERGY_USAGE_SERVICE_NAME, + ENERGY_RETURN_SERVICE_NAME, + ], +) +@pytest.mark.parametrize("date_field", ["start", "end"]) +@pytest.mark.parametrize("date_value", ["incorrect date"]) +async def test_service_validation_invalid_date( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + service: str, + date_field: str, + date_value: str, +) -> None: + """Test invalid date validation for easyEnergy services.""" + service_data: dict[str, str | bool] = { + ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, + date_field: date_value, + } + if service != ENERGY_RETURN_SERVICE_NAME: + service_data["incl_vat"] = True + + with pytest.raises(ServiceValidationError) as err: + await hass.services.async_call( + DOMAIN, + service, + service_data, + blocking=True, + return_response=True, + ) + + assert str(err.value) == f"Invalid date provided. Got {date_value}" + assert err.value.translation_key == "invalid_date" + assert err.value.translation_placeholders == {"date": date_value} From 6384e6b38d9b51296c8b07234c10ec6b759eae94 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:45:08 +0200 Subject: [PATCH 7/8] Add alarm control panel platform to UniFi Protect (#169158) Co-authored-by: RaHehl --- .../components/unifiprotect/__init__.py | 7 + .../unifiprotect/alarm_control_panel.py | 118 +++++++ .../components/unifiprotect/const.py | 4 + homeassistant/components/unifiprotect/data.py | 19 ++ .../components/unifiprotect/icons.json | 5 + .../components/unifiprotect/strings.json | 8 + .../components/unifiprotect/utils.py | 2 + tests/components/unifiprotect/conftest.py | 9 + .../unifiprotect/test_alarm_control_panel.py | 313 ++++++++++++++++++ tests/components/unifiprotect/utils.py | 1 + 10 files changed, 486 insertions(+) create mode 100644 homeassistant/components/unifiprotect/alarm_control_panel.py create mode 100644 tests/components/unifiprotect/test_alarm_control_panel.py diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 2d6273dc551d3..aae41d2052fca 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -158,6 +158,13 @@ async def _async_setup_entry( await async_migrate_data(hass, entry, data_service.api, bootstrap) data_service.async_setup() + # Prime the public bootstrap. The devices websocket subscription was already + # registered in async_setup() per library docs (subscribe first, then prime). + try: + await data_service.api.update_public() + except Exception: # noqa: BLE001 + _LOGGER.debug("Public API bootstrap update failed", exc_info=True) + # Load PTZ patrol data before loading platforms await data_service.async_load_ptz_patrols() diff --git a/homeassistant/components/unifiprotect/alarm_control_panel.py b/homeassistant/components/unifiprotect/alarm_control_panel.py new file mode 100644 index 0000000000000..c1ecb6e23ea2c --- /dev/null +++ b/homeassistant/components/unifiprotect/alarm_control_panel.py @@ -0,0 +1,118 @@ +"""Support for UniFi Protect NVR alarm control panel.""" + +from __future__ import annotations + +from uiprotect.data import NVR, NvrArmModeStatus +from uiprotect.exceptions import GlobalAlarmManagerError + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, + AlarmControlPanelState, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .data import ProtectData, ProtectDeviceType, UFPConfigEntry +from .entity import ProtectNVREntity +from .utils import async_ufp_instance_command + +PARALLEL_UPDATES = 0 + +_UIPROTECT_TO_HA: dict[NvrArmModeStatus, AlarmControlPanelState] = { + NvrArmModeStatus.DISABLED: AlarmControlPanelState.DISARMED, + NvrArmModeStatus.ARMING: AlarmControlPanelState.ARMING, + NvrArmModeStatus.ARMED: AlarmControlPanelState.ARMED_AWAY, + NvrArmModeStatus.BREACH: AlarmControlPanelState.TRIGGERED, + NvrArmModeStatus.UNKNOWN: AlarmControlPanelState.DISARMED, +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: UFPConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up alarm control panel for UniFi Protect NVR.""" + data = entry.runtime_data + api = data.api + + # No public Integration API available (e.g. older NVR firmware that does + # not expose the Alarm Manager endpoint, or no API key configured). + # Skip entity creation entirely; we cannot represent the alarm state. + if not api.has_public_bootstrap: + return + + # ``arm_mode`` is ``None`` on NVR firmware that predates the Alarm Manager + # public API. Skip entity creation so the user does not see a permanently + # unavailable entity. + if api.public_bootstrap.arm_mode is None: + return + + nvr = api.bootstrap.nvr + async_add_entities([ProtectNVRAlarmControlPanel(data, device=nvr)]) + + +class ProtectNVRAlarmControlPanel(ProtectNVREntity, AlarmControlPanelEntity): + """UniFi Protect NVR Alarm Control Panel.""" + + _attr_code_arm_required = False + _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + _attr_translation_key = "nvr_alarm" + _state_attrs = ("_attr_available", "_attr_alarm_state") + + def __init__(self, data: ProtectData, device: NVR) -> None: + """Initialize the alarm control panel.""" + super().__init__(data, device, EntityDescription(key="alarm")) + self._refresh_alarm_state() + + @callback + def _refresh_alarm_state(self) -> None: + """Update _attr_alarm_state from the public bootstrap cache.""" + api = self.data.api + arm_mode = api.public_bootstrap.arm_mode if api.has_public_bootstrap else None + if arm_mode is None: + # No alarm data available — force unavailable regardless of the + # private WebSocket state managed by the base class. + self._attr_available = False + self._attr_alarm_state = None + return + # Do NOT set _attr_available = True here. Availability when alarm data + # is present is determined exclusively by the base class via + # last_update_success (private WebSocket health). Only force it to + # False as an additional condition when alarm data is missing. + # Fall back to DISARMED for unknown future status values rather than + # rendering the entity as ``unknown``. + self._attr_alarm_state = _UIPROTECT_TO_HA.get( + arm_mode.status, AlarmControlPanelState.DISARMED + ) + + @callback + def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None: + super()._async_update_device_from_protect(device) + self._refresh_alarm_state() + + @async_ufp_instance_command + async def async_alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + try: + await self.data.api.disable_arm_alarm_public() + except GlobalAlarmManagerError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="global_alarm_manager", + ) from err + + @async_ufp_instance_command + async def async_alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command (arms with the currently selected profile).""" + try: + await self.data.api.enable_arm_alarm_public() + except GlobalAlarmManagerError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="global_alarm_manager", + ) from err diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index a1b1510ee140d..c70efc82532e6 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -52,6 +52,9 @@ DEVICES_WITH_ENTITIES = DEVICES_THAT_ADOPT | {ModelType.NVR} DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT} +# Public API devices WebSocket: NVR (for arm_mode updates). +DEVICES_WS_SUBSCRIBED_MODELS = {ModelType.NVR} + MIN_REQUIRED_PROTECT_V = Version("6.0.0") OUTDATED_LOG_MESSAGE = ( "You are running v%s of UniFi Protect. Minimum required version is v%s. Please" @@ -61,6 +64,7 @@ TYPE_EMPTY_VALUE = "" PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 8d3abdc768c3d..8dc6979104f75 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -164,8 +164,27 @@ def async_setup(self) -> None: async_track_time_interval( self._hass, self._async_poll, self._update_interval ), + # Subscribe to the public devices websocket unconditionally so that + # it is active before update_public() primes the cache. + # Per library docs: subscribe first, then call update_public(). + api.subscribe_devices_websocket( + self._async_process_public_devices_ws_message + ), ] + @callback + def _async_process_public_devices_ws_message( + self, message: WSSubscriptionMessage + ) -> None: + """Process a message from the public devices websocket. + + The API client pre-filters messages to ModelType.NVR via + DEVICES_WS_SUBSCRIBED_MODELS, so every message here is an NVR update. + The library has already merged the arm_mode into the PublicNVR cache; + signal the private NVR so alarm entities pick up the new state. + """ + self._async_signal_device_update(self.api.bootstrap.nvr) + @callback def _async_websocket_state_changed(self, state: WebsocketState) -> None: """Handle a change in the websocket state.""" diff --git a/homeassistant/components/unifiprotect/icons.json b/homeassistant/components/unifiprotect/icons.json index f66a963da4e39..9c0b7a2732e2f 100644 --- a/homeassistant/components/unifiprotect/icons.json +++ b/homeassistant/components/unifiprotect/icons.json @@ -1,5 +1,10 @@ { "entity": { + "alarm_control_panel": { + "nvr_alarm": { + "default": "mdi:shield-home" + } + }, "binary_sensor": { "alarm_sound_detection": { "default": "mdi:alarm-bell" diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 69ac175ae39aa..e507acf0fc16a 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -90,6 +90,11 @@ } }, "entity": { + "alarm_control_panel": { + "nvr_alarm": { + "name": "Alarm Manager" + } + }, "binary_sensor": { "alarm_sound_detection": { "name": "Alarm sound detection" @@ -668,6 +673,9 @@ "device_not_found": { "message": "No device found for device id: {device_id}" }, + "global_alarm_manager": { + "message": "The alarm manager on this UniFi Protect NVR is set to Global mode and cannot be controlled locally." + }, "no_users_found": { "message": "No users found, please check Protect permissions" }, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index b520e83a59287..b6c35aaed943e 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -37,6 +37,7 @@ CONF_ALL_UPDATES, CONF_OVERRIDE_CHOST, DEVICES_FOR_SUBSCRIBE, + DEVICES_WS_SUBSCRIBED_MODELS, DOMAIN, ModelType, ) @@ -126,6 +127,7 @@ def async_create_api_client( session=session, public_api_session=public_api_session, subscribed_models=DEVICES_FOR_SUBSCRIBE, + devices_ws_subscribed_models=DEVICES_WS_SUBSCRIBED_MODELS, override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), ignore_unadopted=False, diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index e99989d4abb85..6f1e683831a48 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -185,8 +185,17 @@ def subscribe_websocket_state( ufp.ws_state_subscription = ws_state_subscription return Mock() + def subscribe_devices_websocket( + ws_callback: Callable[[WSSubscriptionMessage], None], + ) -> Any: + ufp.devices_ws_subscription = ws_callback + return Mock() + ufp_client.subscribe_websocket = subscribe ufp_client.subscribe_websocket_state = subscribe_websocket_state + ufp_client.subscribe_devices_websocket = subscribe_devices_websocket + ufp_client.update_public = AsyncMock() + ufp_client.has_public_bootstrap = False yield ufp diff --git a/tests/components/unifiprotect/test_alarm_control_panel.py b/tests/components/unifiprotect/test_alarm_control_panel.py new file mode 100644 index 0000000000000..83634d04f9b8f --- /dev/null +++ b/tests/components/unifiprotect/test_alarm_control_panel.py @@ -0,0 +1,313 @@ +"""Test the UniFi Protect alarm control panel platform.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, Mock + +import pytest +from uiprotect.data import NVR, NvrArmMode, NvrArmModeStatus, PublicBootstrap +from uiprotect.exceptions import GlobalAlarmManagerError +from uiprotect.websocket import WebsocketState + +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_DOMAIN, + AlarmControlPanelState, +) +from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ENTITY_ID, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_DISARM, + STATE_UNAVAILABLE, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from .utils import MockUFPFixture, assert_entity_counts, init_entry + +ALARM_ENTITY_ID = "alarm_control_panel.unifiprotect_alarm_manager" + + +def _make_arm_mode(status: NvrArmModeStatus) -> Mock: + """Create a NvrArmMode object for testing.""" + arm_mode = Mock(spec=NvrArmMode) + arm_mode.status = status + return arm_mode + + +def _make_public_bootstrap(arm_mode: Mock | None) -> Mock: + """Create a PublicBootstrap with the given arm_mode.""" + pb = Mock(spec=PublicBootstrap) + pb.arm_mode = arm_mode + pb.arm_profiles = {} + return pb + + +async def test_alarm_panel_not_created_without_public_bootstrap( + hass: HomeAssistant, + ufp: MockUFPFixture, +) -> None: + """Alarm panel entity is NOT created when has_public_bootstrap is False.""" + ufp.api.has_public_bootstrap = False + + await init_entry(hass, ufp, []) + assert_entity_counts(hass, Platform.ALARM_CONTROL_PANEL, 0, 0) + + +async def test_alarm_panel_created_with_public_bootstrap( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + ufp: MockUFPFixture, + nvr: NVR, +) -> None: + """Alarm panel entity IS created when has_public_bootstrap is True.""" + arm_mode = _make_arm_mode(NvrArmModeStatus.DISABLED) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + + await init_entry(hass, ufp, []) + assert_entity_counts(hass, Platform.ALARM_CONTROL_PANEL, 1, 1) + + entity = entity_registry.async_get(ALARM_ENTITY_ID) + assert entity is not None + assert entity.unique_id == f"{nvr.mac}_alarm" + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == AlarmControlPanelState.DISARMED + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + +@pytest.mark.parametrize( + ("nvr_status", "expected_state"), + [ + (NvrArmModeStatus.DISABLED, AlarmControlPanelState.DISARMED), + (NvrArmModeStatus.UNKNOWN, AlarmControlPanelState.DISARMED), + (NvrArmModeStatus.ARMING, AlarmControlPanelState.ARMING), + (NvrArmModeStatus.ARMED, AlarmControlPanelState.ARMED_AWAY), + (NvrArmModeStatus.BREACH, AlarmControlPanelState.TRIGGERED), + ], +) +async def test_alarm_panel_state_mapping( + hass: HomeAssistant, + ufp: MockUFPFixture, + nvr_status: NvrArmModeStatus, + expected_state: AlarmControlPanelState, +) -> None: + """Test that NvrArmModeStatus maps to correct AlarmControlPanelState.""" + arm_mode = _make_arm_mode(nvr_status) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + + await init_entry(hass, ufp, []) + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == expected_state + + +async def test_alarm_panel_not_created_without_arm_mode( + hass: HomeAssistant, + ufp: MockUFPFixture, +) -> None: + """Alarm panel entity is NOT created on old firmware (arm_mode is None).""" + pb = _make_public_bootstrap(arm_mode=None) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + + await init_entry(hass, ufp, []) + assert_entity_counts(hass, Platform.ALARM_CONTROL_PANEL, 0, 0) + + +async def test_alarm_panel_disarm( + hass: HomeAssistant, + ufp: MockUFPFixture, +) -> None: + """Test that disarm service calls disable_arm_alarm_public.""" + arm_mode = _make_arm_mode(NvrArmModeStatus.ARMED) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + ufp.api.disable_arm_alarm_public = AsyncMock() + + await init_entry(hass, ufp, []) + + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_DISARM, + {ATTR_ENTITY_ID: ALARM_ENTITY_ID}, + blocking=True, + ) + + ufp.api.disable_arm_alarm_public.assert_called_once() + + +async def test_alarm_panel_arm_away( + hass: HomeAssistant, + ufp: MockUFPFixture, +) -> None: + """Test that arm_away service calls enable_arm_alarm_public.""" + arm_mode = _make_arm_mode(NvrArmModeStatus.DISABLED) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + ufp.api.enable_arm_alarm_public = AsyncMock() + + await init_entry(hass, ufp, []) + + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_ARM_AWAY, + {ATTR_ENTITY_ID: ALARM_ENTITY_ID}, + blocking=True, + ) + + ufp.api.enable_arm_alarm_public.assert_called_once() + + +async def test_alarm_panel_disarm_global_manager_error( + hass: HomeAssistant, + ufp: MockUFPFixture, +) -> None: + """Test that GlobalAlarmManagerError on disarm raises HomeAssistantError.""" + arm_mode = _make_arm_mode(NvrArmModeStatus.ARMED) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + ufp.api.disable_arm_alarm_public = AsyncMock(side_effect=GlobalAlarmManagerError()) + + await init_entry(hass, ufp, []) + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_DISARM, + {ATTR_ENTITY_ID: ALARM_ENTITY_ID}, + blocking=True, + ) + assert exc_info.value.translation_key == "global_alarm_manager" + + +async def test_alarm_panel_arm_away_global_manager_error( + hass: HomeAssistant, + ufp: MockUFPFixture, +) -> None: + """Test that GlobalAlarmManagerError on arm raises HomeAssistantError.""" + arm_mode = _make_arm_mode(NvrArmModeStatus.DISABLED) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + ufp.api.enable_arm_alarm_public = AsyncMock(side_effect=GlobalAlarmManagerError()) + + await init_entry(hass, ufp, []) + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_ARM_AWAY, + {ATTR_ENTITY_ID: ALARM_ENTITY_ID}, + blocking=True, + ) + assert exc_info.value.translation_key == "global_alarm_manager" + + +async def test_alarm_panel_state_update_via_ws( + hass: HomeAssistant, + ufp: MockUFPFixture, + nvr: NVR, +) -> None: + """Test that public devices WS update triggers state refresh.""" + arm_mode = _make_arm_mode(NvrArmModeStatus.DISABLED) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + + await init_entry(hass, ufp, []) + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == AlarmControlPanelState.DISARMED + + # Simulate arm state change via the public devices websocket + armed_arm_mode = _make_arm_mode(NvrArmModeStatus.ARMED) + pb.arm_mode = armed_arm_mode + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.old_obj = nvr + mock_msg.new_obj = nvr + assert ufp.devices_ws_subscription is not None + ufp.devices_ws_subscription(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == AlarmControlPanelState.ARMED_AWAY + + +async def test_alarm_panel_unavailable_when_arm_mode_disappears( + hass: HomeAssistant, + ufp: MockUFPFixture, + nvr: NVR, +) -> None: + """Entity becomes unavailable when arm_mode disappears after a WS update.""" + arm_mode = _make_arm_mode(NvrArmModeStatus.ARMED) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + + await init_entry(hass, ufp, []) + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == AlarmControlPanelState.ARMED_AWAY + + # Simulate firmware downgrade / global mode switch: arm_mode becomes None + pb.arm_mode = None + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.old_obj = nvr + mock_msg.new_obj = nvr + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_alarm_panel_unavailable_on_ws_disconnect( + hass: HomeAssistant, + ufp: MockUFPFixture, +) -> None: + """Entity becomes unavailable when the private WebSocket disconnects.""" + arm_mode = _make_arm_mode(NvrArmModeStatus.ARMED) + pb = _make_public_bootstrap(arm_mode) + ufp.api.has_public_bootstrap = True + ufp.api.public_bootstrap = pb + + await init_entry(hass, ufp, []) + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == AlarmControlPanelState.ARMED_AWAY + + ufp.ws_state_subscription(WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + ufp.ws_state_subscription(WebsocketState.CONNECTED) + await hass.async_block_till_done() + + state = hass.states.get(ALARM_ENTITY_ID) + assert state is not None + assert state.state == AlarmControlPanelState.ARMED_AWAY diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index a99ad68e785bd..29b52f1ba7871 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -38,6 +38,7 @@ class MockUFPFixture: api: ProtectApiClient ws_subscription: Callable[[WSSubscriptionMessage], None] | None = None ws_state_subscription: Callable[[WebsocketState], None] | None = None + devices_ws_subscription: Callable[[WSSubscriptionMessage], None] | None = None def ws_msg(self, msg: WSSubscriptionMessage) -> None: """Emit WS message for testing.""" From b5a1b592e90c8437e80206245bc772b441aec148 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:44:41 +0200 Subject: [PATCH 8/8] Bump uiprotect to 10.4.1 (#169192) Co-authored-by: RaHehl Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ac8396b654372..215381a8fe15a 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["uiprotect"], "quality_scale": "platinum", - "requirements": ["uiprotect==10.4.0"] + "requirements": ["uiprotect==10.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index abc793907041d..669fb788f1a3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3192,7 +3192,7 @@ uasiren==0.0.1 uhooapi==1.2.8 # homeassistant.components.unifiprotect -uiprotect==10.4.0 +uiprotect==10.4.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e43ec3edac02c..32b255e580119 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2713,7 +2713,7 @@ uasiren==0.0.1 uhooapi==1.2.8 # homeassistant.components.unifiprotect -uiprotect==10.4.0 +uiprotect==10.4.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7