From 63bed905de57fb19464dd2e4ca026077256d8dbb Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 10 Apr 2025 15:34:56 +0200 Subject: [PATCH 1/8] fix: distinguish Integration data by entry_id Signed-off-by: F.N. Claessen --- .../flexmeasures_hacs/__init__.py | 12 +++++------ custom_components/flexmeasures_hacs/sensor.py | 11 +++++----- .../flexmeasures_hacs/services.py | 20 +++++++++---------- .../flexmeasures_hacs/websockets.py | 9 +++++---- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/custom_components/flexmeasures_hacs/__init__.py b/custom_components/flexmeasures_hacs/__init__.py index c99831d..0ff4171 100644 --- a/custom_components/flexmeasures_hacs/__init__.py +++ b/custom_components/flexmeasures_hacs/__init__.py @@ -31,8 +31,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up FlexMeasures from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - # Reload integration when the options are updated entry.async_on_unload(entry.add_update_listener(options_update_listener)) @@ -69,9 +67,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) FRBC_data = FRBC_Config(**frbc_data_dict) - hass.data[DOMAIN][FRBC_CONFIG] = FRBC_data - - hass.data[DOMAIN]["fm_client"] = client + entry_config = { + FRBC_CONFIG: FRBC_data, + "fm_client": client, + } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry_config hass.http.register_view(WebsocketAPIView(entry)) @@ -98,7 +98,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_unload_services(hass) if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id, None) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/custom_components/flexmeasures_hacs/sensor.py b/custom_components/flexmeasures_hacs/sensor.py index 1c66da7..449b4f5 100644 --- a/custom_components/flexmeasures_hacs/sensor.py +++ b/custom_components/flexmeasures_hacs/sensor.py @@ -21,9 +21,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up sensor.""" - hass.data[DOMAIN][SCHEDULE_STATE] = {"schedule": [], "start": None} + hass.data[DOMAIN][entry.entry_id][SCHEDULE_STATE] = {"schedule": [], "start": None} - async_add_entities([FlexMeasuresScheduleSensor()], True) + async_add_entities([FlexMeasuresScheduleSensor(entry_id=entry.entry_id)], True) class FlexMeasuresScheduleSensor(SensorEntity): @@ -32,9 +32,10 @@ class FlexMeasuresScheduleSensor(SensorEntity): _attr_device_class = SensorDeviceClass.POWER _attr_native_unit_of_measurement = UnitOfPower.KILO_WATT - def __init__(self) -> None: + def __init__(self, entry_id) -> None: """Sensor to store the schedule created by FlexMeasures.""" self._attr_unique_id = SCHEDULE_ENTITY + self.entry_id = entry_id @property def name(self) -> str: @@ -45,7 +46,7 @@ def name(self) -> str: def native_value(self) -> float: """Average power.""" - commands = self.hass.data[DOMAIN][SCHEDULE_STATE]["schedule"] + commands = self.hass.data[DOMAIN][self.entry_id][SCHEDULE_STATE]["schedule"] if len(commands) == 0: return 0 return sum(command["value"] for command in commands) / len(commands) @@ -53,7 +54,7 @@ def native_value(self) -> float: @property def extra_state_attributes(self) -> dict[str, Any]: """Return default attributes for the FlexMeasures Schedule sensor.""" - return self.hass.data[DOMAIN][SCHEDULE_STATE] + return self.hass.data[DOMAIN][self.entry_id][SCHEDULE_STATE] async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/custom_components/flexmeasures_hacs/services.py b/custom_components/flexmeasures_hacs/services.py index df78b68..41332fa 100644 --- a/custom_components/flexmeasures_hacs/services.py +++ b/custom_components/flexmeasures_hacs/services.py @@ -81,10 +81,10 @@ async def change_control_type( ): # pylint: disable=possibly-unused-variable """Change control type S2 Protocol.""" - if "cem" not in hass.data[DOMAIN]: + if "cem" not in hass.data[DOMAIN][entry.entry_id]: raise UndefinedCEMError() - cem: CEM = hass.data[DOMAIN]["cem"] + cem: CEM = hass.data[DOMAIN][entry.entry_id]["cem"] control_type = cast(str, call.data.get("control_type")) @@ -102,7 +102,7 @@ async def change_control_type( async def trigger_and_get_schedule( call: ServiceCall, ): # pylint: disable=possibly-unused-variable - client: FlexMeasuresClient = hass.data[DOMAIN]["fm_client"] + client: FlexMeasuresClient = hass.data[DOMAIN][entry.entry_id]["fm_client"] resolution = pd.Timedelta(RESOLUTION) tzinfo = dt_util.get_time_zone(hass.config.time_zone) start = time_ceil(datetime.now(tz=tzinfo), resolution) @@ -146,9 +146,9 @@ async def trigger_and_get_schedule( for i, value in enumerate(schedule["values"]) ] - hass.data[DOMAIN][SCHEDULE_STATE]["schedule"] = schedule - hass.data[DOMAIN][SCHEDULE_STATE]["start"] = start - hass.data[DOMAIN][SCHEDULE_STATE]["duration"] = get_from_option_or_config( + hass.data[DOMAIN][entry.entry_id][SCHEDULE_STATE]["schedule"] = schedule + hass.data[DOMAIN][entry.entry_id][SCHEDULE_STATE]["start"] = start + hass.data[DOMAIN][entry.entry_id][SCHEDULE_STATE]["duration"] = get_from_option_or_config( "schedule_duration", entry ) @@ -157,7 +157,7 @@ async def trigger_and_get_schedule( async def post_measurements( call: ServiceCall, ): # pylint: disable=possibly-unused-variable - client: FlexMeasuresClient = hass.data[DOMAIN]["fm_client"] + client: FlexMeasuresClient = hass.data[DOMAIN][entry.entry_id]["fm_client"] await client.post_measurements( sensor_id=call.data.get("sensor_id"), @@ -173,10 +173,10 @@ async def send_frbc_instruction( ): # pylint: disable=possibly-unused-variable """Send S2 Fill Rate Based Control message to the ResourceManager""" - if "cem" not in hass.data[DOMAIN]: + if "cem" not in hass.data[DOMAIN][entry.entry_id]: raise UndefinedCEMError() - cem: CEM = hass.data[DOMAIN]["cem"] + cem: CEM = hass.data[DOMAIN][entry.entry_id]["cem"] tz = pytz.timezone(hass.config.time_zone) DT_FMT = "%Y-%m-%d %H:%M:%S" @@ -202,7 +202,7 @@ async def send_frbc_instruction( async def get_measurements( call: ServiceCall, ) -> ServiceResponse: # pylint: disable=possibly-unused-variable - client: FlexMeasuresClient = hass.data[DOMAIN]["fm_client"] + client: FlexMeasuresClient = hass.data[DOMAIN][entry.entry_id]["fm_client"] response = await client.get_sensor_data( sensor_id=call.data.get("sensor_id"), diff --git a/custom_components/flexmeasures_hacs/websockets.py b/custom_components/flexmeasures_hacs/websockets.py index 02eb78d..58af07a 100644 --- a/custom_components/flexmeasures_hacs/websockets.py +++ b/custom_components/flexmeasures_hacs/websockets.py @@ -17,6 +17,7 @@ from s2python.common import EnergyManagementRole, Handshake, ControlType from homeassistant.components.http import HomeAssistantView +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DOMAIN, WS_VIEW_NAME, WS_VIEW_URI @@ -60,7 +61,7 @@ class WebSocketHandler: cem: CEM - def __init__(self, hass: HomeAssistant, entry, request: web.Request) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, request: web.Request) -> None: """Initialize an active connection.""" self.hass = hass self.request = request @@ -68,12 +69,12 @@ def __init__(self, hass: HomeAssistant, entry, request: web.Request) -> None: self.wsock = web.WebSocketResponse(heartbeat=None) self.cem = CEM( - fm_client=hass.data[DOMAIN]["fm_client"], + fm_client=hass.data[DOMAIN][entry.entry_id]["fm_client"], default_control_type=ControlType.FILL_RATE_BASED_CONTROL, ) - frbc_data: FRBC_Config = hass.data[DOMAIN]["frbc_config"] + frbc_data: FRBC_Config = hass.data[DOMAIN][entry.entry_id]["frbc_config"] frbc = FillRateBasedControlTUNES(**asdict(frbc_data)) - hass.data[DOMAIN]["cem"] = self.cem + hass.data[DOMAIN][entry.entry_id]["cem"] = self.cem self.cem.register_control_type(frbc) self._logger = WebSocketAdapter(_WS_LOGGER, {"connid": id(self)}) From 5691c0039939a547a76d8ecee712aa2c54003537 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 10 Apr 2025 15:43:59 +0200 Subject: [PATCH 2/8] style: ruff-format Signed-off-by: F.N. Claessen --- custom_components/flexmeasures_hacs/services.py | 4 ++-- custom_components/flexmeasures_hacs/websockets.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/flexmeasures_hacs/services.py b/custom_components/flexmeasures_hacs/services.py index 41332fa..3e9ed1a 100644 --- a/custom_components/flexmeasures_hacs/services.py +++ b/custom_components/flexmeasures_hacs/services.py @@ -148,8 +148,8 @@ async def trigger_and_get_schedule( hass.data[DOMAIN][entry.entry_id][SCHEDULE_STATE]["schedule"] = schedule hass.data[DOMAIN][entry.entry_id][SCHEDULE_STATE]["start"] = start - hass.data[DOMAIN][entry.entry_id][SCHEDULE_STATE]["duration"] = get_from_option_or_config( - "schedule_duration", entry + hass.data[DOMAIN][entry.entry_id][SCHEDULE_STATE]["duration"] = ( + get_from_option_or_config("schedule_duration", entry) ) async_dispatcher_send(hass, SIGNAL_UPDATE_SCHEDULE) diff --git a/custom_components/flexmeasures_hacs/websockets.py b/custom_components/flexmeasures_hacs/websockets.py index 58af07a..23e077b 100644 --- a/custom_components/flexmeasures_hacs/websockets.py +++ b/custom_components/flexmeasures_hacs/websockets.py @@ -61,7 +61,9 @@ class WebSocketHandler: cem: CEM - def __init__(self, hass: HomeAssistant, entry: ConfigEntry, request: web.Request) -> None: + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, request: web.Request + ) -> None: """Initialize an active connection.""" self.hass = hass self.request = request From a24a97e97425bfe10ccc9d20dbe8c7d3d891f099 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 10 Apr 2025 16:04:28 +0200 Subject: [PATCH 3/8] fix: tests? Signed-off-by: F.N. Claessen --- custom_components/flexmeasures_hacs/services.py | 2 +- custom_components/flexmeasures_hacs/websockets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/flexmeasures_hacs/services.py b/custom_components/flexmeasures_hacs/services.py index 3e9ed1a..1a2fce2 100644 --- a/custom_components/flexmeasures_hacs/services.py +++ b/custom_components/flexmeasures_hacs/services.py @@ -96,7 +96,7 @@ async def change_control_type( await cem.activate_control_type(control_type=control_type) hass.states.async_set( - f"{DOMAIN}.cem", json.dumps({"control_type": str(cem.control_type)}) + f"{DOMAIN}.{entry.entry_id}.cem", json.dumps({"control_type": str(cem.control_type)}) ) async def trigger_and_get_schedule( diff --git a/custom_components/flexmeasures_hacs/websockets.py b/custom_components/flexmeasures_hacs/websockets.py index 23e077b..42dd67a 100644 --- a/custom_components/flexmeasures_hacs/websockets.py +++ b/custom_components/flexmeasures_hacs/websockets.py @@ -33,7 +33,7 @@ class WebsocketAPIView(HomeAssistantView): url: str = WS_VIEW_URI requires_auth: bool = False - def __init__(self, entry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize websocket view.""" super().__init__() self.entry = entry From d89f726815f4ce4011e88107ff0c9f16793313a6 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 10 Apr 2025 16:09:52 +0200 Subject: [PATCH 4/8] style: ruff-format again Signed-off-by: F.N. Claessen --- custom_components/flexmeasures_hacs/services.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/flexmeasures_hacs/services.py b/custom_components/flexmeasures_hacs/services.py index 1a2fce2..a24a188 100644 --- a/custom_components/flexmeasures_hacs/services.py +++ b/custom_components/flexmeasures_hacs/services.py @@ -96,7 +96,8 @@ async def change_control_type( await cem.activate_control_type(control_type=control_type) hass.states.async_set( - f"{DOMAIN}.{entry.entry_id}.cem", json.dumps({"control_type": str(cem.control_type)}) + f"{DOMAIN}.{entry.entry_id}.cem", + json.dumps({"control_type": str(cem.control_type)}), ) async def trigger_and_get_schedule( From d287201c8b57b97a8e5403b8e8a7801523edc40a Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 10 Apr 2025 16:40:02 +0200 Subject: [PATCH 5/8] fix: `ERROR homeassistant.config_entries:config_entries.py:1663 Config entry 'Mock Title' from integration flexmeasures_hacs has an invalid unique_id '1212121' of type int when a string is expected` Signed-off-by: F.N. Claessen --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index a876f51..766f397 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,6 @@ async def setup_fm_integration(hass: HomeAssistant): "soc_min": 0.0, "soc_max": 0.001, }, - unique_id=1212121, state=ConfigEntryState.NOT_LOADED, ) From 558c05e5c75a3821fc6e3830270d1d14ebba161e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 10 Apr 2025 16:43:46 +0200 Subject: [PATCH 6/8] dev: check entry_id in fixture Signed-off-by: F.N. Claessen --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 766f397..9d576c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,6 +48,8 @@ async def setup_fm_integration(hass: HomeAssistant): entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() + assert entry.entry_id in hass.data[DOMAIN] + print(f"ENTRY ID = {entry.entry_id}") return entry From 40afa32531d3d38ba7f9e5d9a893a7ab73468158 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 10 Apr 2025 16:50:27 +0200 Subject: [PATCH 7/8] dev: scope MockConfigEntry to session Signed-off-by: F.N. Claessen --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9d576c4..631581c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def auto_enable_custom_integrations(enable_custom_integrations): yield -@pytest.fixture +@pytest.fixture(scope="session") async def setup_fm_integration(hass: HomeAssistant): """FlexMeasures integration setup.""" entry = MockConfigEntry( @@ -50,6 +50,7 @@ async def setup_fm_integration(hass: HomeAssistant): await hass.async_block_till_done() assert entry.entry_id in hass.data[DOMAIN] print(f"ENTRY ID = {entry.entry_id}") + print(f"HASS DATA FOR OUR DOMAIN = {hass.data[DOMAIN]}") return entry From 6d47c9cbeb06b897140f2c448930938aea84366f Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 10 Apr 2025 16:55:03 +0200 Subject: [PATCH 8/8] dev: revert scoping MockConfigEntry to session, but still log hass.data[DOMAIN] Signed-off-by: F.N. Claessen --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 631581c..bec0422 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def auto_enable_custom_integrations(enable_custom_integrations): yield -@pytest.fixture(scope="session") +@pytest.fixture async def setup_fm_integration(hass: HomeAssistant): """FlexMeasures integration setup.""" entry = MockConfigEntry(