From d23fd3c95185720a6a6e50e140726a5435bcc5a1 Mon Sep 17 00:00:00 2001 From: "Sietse T. Au" Date: Tue, 23 Jan 2024 22:16:09 +0100 Subject: [PATCH 1/4] Initial multi-entry approach. Basically we maintain the domain, while for all the other places we use the unique_id for separate instances. Currently the unique_id is generated from the conf_name. For backwards compatibility you would like to be able to set it to "uponor". --- custom_components/uponor/__init__.py | 25 ++++++++++++++++--------- custom_components/uponor/climate.py | 20 ++++++++++++++------ custom_components/uponor/config_flow.py | 8 +++++++- custom_components/uponor/helper.py | 11 +++++++++++ custom_components/uponor/switch.py | 23 +++++++++++++++-------- 5 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 custom_components/uponor/helper.py diff --git a/custom_components/uponor/__init__.py b/custom_components/uponor/__init__.py index 3008540..aad688f 100644 --- a/custom_components/uponor/__init__.py +++ b/custom_components/uponor/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -37,24 +37,30 @@ DEFAULT_TEMP ) +from .helper import ( + get_unique_id_from +) + _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: dict): - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN]["config"] = config.get(DOMAIN) or {} + # just return True, we're not using this! + # hass.data.setdefault(DOMAIN, {}) + # hass.data[DOMAIN]["config"] = config.get(DOMAIN) or {} return True async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): host = config_entry.data[CONF_HOST] + unique_id = get_unique_id_from(config_entry.data[CONF_NAME]) store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - state_proxy = await hass.async_add_executor_job(lambda: UponorStateProxy(hass, host, store)) + state_proxy = await hass.async_add_executor_job(lambda: UponorStateProxy(hass, host, store, unique_id)) await state_proxy.async_update(0) thermostats = state_proxy.get_active_thermostats() - hass.data[DOMAIN] = { + hass.data[unique_id] = { "state_proxy": state_proxy, "thermostats": thermostats } @@ -62,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): def handle_set_variable(call): var_name = call.data.get('var_name') var_value = call.data.get('var_value') - hass.data[DOMAIN]['state_proxy'].set_variable(var_name, var_value) + hass.data[unique_id]['state_proxy'].set_variable(var_name, var_value) hass.services.async_register(DOMAIN, "set_variable", handle_set_variable) @@ -75,12 +81,13 @@ def handle_set_variable(call): class UponorStateProxy: - def __init__(self, hass, host, store): + def __init__(self, hass, host, store, unique_id): self._hass = hass self._client = UponorJnap(host) self._store = store self._data = {} self._storage_data = {} + self._unique_id = unique_id # Thermostats config @@ -217,7 +224,7 @@ def get_status(self, thermostat): # HVAC modes async def async_switch_to_cooling(self): - for thermostat in self._hass.data[DOMAIN]['thermostats']: + for thermostat in self._hass.data[self._unique_id]['thermostats']: if self.get_setpoint(thermostat) == self.get_min_limit(thermostat): await self._hass.async_add_executor_job( lambda: self.set_setpoint(thermostat, self.get_max_limit(thermostat))) @@ -227,7 +234,7 @@ async def async_switch_to_cooling(self): async_dispatcher_send(self._hass, SIGNAL_UPONOR_STATE_UPDATE) async def async_switch_to_heating(self): - for thermostat in self._hass.data[DOMAIN]['thermostats']: + for thermostat in self._hass.data[self._unique_id]['thermostats']: if self.get_setpoint(thermostat) == self.get_max_limit(thermostat): await self._hass.async_add_executor_job( lambda: self.set_setpoint(thermostat, self.get_min_limit(thermostat))) diff --git a/custom_components/uponor/climate.py b/custom_components/uponor/climate.py index bd36d06..cc50e59 100644 --- a/custom_components/uponor/climate.py +++ b/custom_components/uponor/climate.py @@ -24,31 +24,39 @@ ) from .const import ( - DOMAIN, SIGNAL_UPONOR_STATE_UPDATE, DEVICE_MANUFACTURER ) +from homeassistant.const import CONF_NAME + +from .helper import ( + get_unique_id_from +) + _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): - state_proxy = hass.data[DOMAIN]["state_proxy"] + unique_id = get_unique_id_from(entry.data[CONF_NAME]) + + state_proxy = hass.data[unique_id]["state_proxy"] entities = [] - for thermostat in hass.data[DOMAIN]["thermostats"]: + for thermostat in hass.data[unique_id]["thermostats"]: if thermostat.lower() in entry.data: name = entry.data[thermostat.lower()] else: name = state_proxy.get_room_name(thermostat) - entities.append(UponorClimate(state_proxy, thermostat, name)) + entities.append(UponorClimate(unique_id, state_proxy, thermostat, name)) if entities: async_add_entities(entities, update_before_add=False) class UponorClimate(ClimateEntity): - def __init__(self, state_proxy, thermostat, name): + def __init__(self, unique_instance_id, state_proxy, thermostat, name): + self._unique_instance_id = unique_instance_id self._state_proxy = state_proxy self._thermostat = thermostat self._name = name @@ -164,7 +172,7 @@ def unique_id(self): @property def device_info(self): return { - "identifiers": {(DOMAIN, self._state_proxy.get_thermostat_id(self._thermostat))}, + "identifiers": {(self._unique_instance_id, self._state_proxy.get_thermostat_id(self._thermostat))}, "name": self._name, "manufacturer": DEVICE_MANUFACTURER, "model": self._state_proxy.get_model(), diff --git a/custom_components/uponor/config_flow.py b/custom_components/uponor/config_flow.py index dcaf043..dd78347 100644 --- a/custom_components/uponor/config_flow.py +++ b/custom_components/uponor/config_flow.py @@ -16,6 +16,10 @@ DEVICE_MANUFACTURER ) +from .helper import ( + get_unique_id_from +) + _LOGGER = logging.getLogger(__name__) @@ -36,7 +40,9 @@ def schema(self): async def async_step_user(self, user_input=None): """Handle the initial step.""" if user_input is not None: - await self.async_set_unique_id(DOMAIN) + unique_id = get_unique_id_from(user_input[CONF_NAME]) + + await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() try: diff --git a/custom_components/uponor/helper.py b/custom_components/uponor/helper.py new file mode 100644 index 0000000..59e4fb1 --- /dev/null +++ b/custom_components/uponor/helper.py @@ -0,0 +1,11 @@ +from .const import ( + DOMAIN +) + + +def get_unique_id_from(conf_name: str): + + raw_unique_id = DOMAIN + "_" + conf_name + cleaned_unique_id = raw_unique_id.replace(" ", "_").lower() + + return cleaned_unique_id diff --git a/custom_components/uponor/switch.py b/custom_components/uponor/switch.py index 10cd687..616c9d1 100644 --- a/custom_components/uponor/switch.py +++ b/custom_components/uponor/switch.py @@ -4,26 +4,32 @@ from homeassistant.const import CONF_NAME from .const import ( - DOMAIN, SIGNAL_UPONOR_STATE_UPDATE, DEVICE_MANUFACTURER ) +from .helper import ( + get_unique_id_from +) + async def async_setup_entry(hass, entry, async_add_entities): - state_proxy = hass.data[DOMAIN]["state_proxy"] - entities = [AwaySwitch(state_proxy, entry.data[CONF_NAME])] + unique_id = get_unique_id_from(entry.data[CONF_NAME]) + + state_proxy = hass.data[unique_id]["state_proxy"] + entities = [AwaySwitch(unique_id, state_proxy, entry.data[CONF_NAME])] if state_proxy.is_cool_available(): - entities.append(CoolSwitch(state_proxy, entry.data[CONF_NAME])) + entities.append(CoolSwitch(unique_id, state_proxy, entry.data[CONF_NAME])) async_add_entities(entities) class AwaySwitch(SwitchEntity): - def __init__(self, state_proxy, name): + def __init__(self, unique_instance_id, state_proxy, name): self._state_proxy = state_proxy self._name = name + self._unique_instance_id = unique_instance_id @property def name(self) -> str: @@ -63,7 +69,7 @@ def unique_id(self): @property def device_info(self): return { - "identifiers": {(DOMAIN, "c")}, + "identifiers": {(self._unique_instance_id, "c")}, "name": self._name, "manufacturer": DEVICE_MANUFACTURER, "model": self._state_proxy.get_model(), @@ -71,9 +77,10 @@ def device_info(self): class CoolSwitch(SwitchEntity): - def __init__(self, state_proxy, name): + def __init__(self, unique_instance_id, state_proxy, name): self._state_proxy = state_proxy self._name = name + self._unique_instance_id = unique_instance_id @property def name(self) -> str: @@ -113,7 +120,7 @@ def unique_id(self): @property def device_info(self): return { - "identifiers": {(DOMAIN, "c")}, + "identifiers": {(self._unique_instance_id, "c")}, "name": self._name, "manufacturer": DEVICE_MANUFACTURER, "model": self._state_proxy.get_model(), From a58e2cbb2c385fc2f3bfbb4594a0ffd88ae62025 Mon Sep 17 00:00:00 2001 From: "Sietse T. Au" Date: Wed, 24 Jan 2024 21:32:36 +0100 Subject: [PATCH 2/4] add unique_id as user input --- custom_components/uponor/config_flow.py | 8 ++++++- custom_components/uponor/const.py | 2 ++ custom_components/uponor/helper.py | 21 ++++++++++++++++++- custom_components/uponor/translations/en.json | 3 ++- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/custom_components/uponor/config_flow.py b/custom_components/uponor/config_flow.py index dd78347..eae5ed6 100644 --- a/custom_components/uponor/config_flow.py +++ b/custom_components/uponor/config_flow.py @@ -12,11 +12,14 @@ from .const import ( DOMAIN, + CONF_UNIQUE_ID, SIGNAL_UPONOR_STATE_UPDATE, DEVICE_MANUFACTURER ) from .helper import ( + create_unique_id_from_user_input, + generate_unique_id_from_user_input, get_unique_id_from ) @@ -34,13 +37,16 @@ def schema(self): { vol.Required(CONF_HOST): str, vol.Required(CONF_NAME, default=DEVICE_MANUFACTURER): str, + vol.Optional(CONF_UNIQUE_ID): str } ) async def async_step_user(self, user_input=None): """Handle the initial step.""" if user_input is not None: - unique_id = get_unique_id_from(user_input[CONF_NAME]) + unique_id = create_unique_id_from_user_input(user_input) + if unique_id is None: + unique_id = generate_unique_id_from_user_input(user_input) await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() diff --git a/custom_components/uponor/const.py b/custom_components/uponor/const.py index aa6e0eb..16fe8a4 100644 --- a/custom_components/uponor/const.py +++ b/custom_components/uponor/const.py @@ -1,5 +1,7 @@ from datetime import timedelta +CONF_UNIQUE_ID = "unique_id" + DOMAIN = "uponor" SIGNAL_UPONOR_STATE_UPDATE = "uponor_state_update" diff --git a/custom_components/uponor/helper.py b/custom_components/uponor/helper.py index 59e4fb1..145e912 100644 --- a/custom_components/uponor/helper.py +++ b/custom_components/uponor/helper.py @@ -1,7 +1,26 @@ from .const import ( - DOMAIN + DOMAIN, + CONF_UNIQUE_ID ) +from homeassistant.const import ( + CONF_NAME +) + + +def create_unique_id_from_user_input(user_input): + if CONF_UNIQUE_ID not in user_input and user_input[CONF_UNIQUE_ID] != "": + return user_input[CONF_UNIQUE_ID] + + return None + + +def generate_unique_id_from_user_input(user_input): + conf_name = user_input[CONF_NAME] + raw_unique_id = DOMAIN + "_" + conf_name + cleaned_unique_id = raw_unique_id.replace(" ", "_").lower() + return cleaned_unique_id + def get_unique_id_from(conf_name: str): diff --git a/custom_components/uponor/translations/en.json b/custom_components/uponor/translations/en.json index f2f2b43..368f786 100644 --- a/custom_components/uponor/translations/en.json +++ b/custom_components/uponor/translations/en.json @@ -6,7 +6,8 @@ "description": "Set up Uponor heating/cooling system", "data": { "host": "Host or IP address of the Uponor controller", - "name": "Name of the integration instance" + "name": "Name of the integration instance", + "unique_id": "Optional unique identifier (must be unique across entries)" } }, "rooms": { From d9e2c6e417347ebd5eda903850d803af14a7c3c2 Mon Sep 17 00:00:00 2001 From: "Sietse T. Au" Date: Wed, 24 Jan 2024 21:48:25 +0100 Subject: [PATCH 3/4] generate unique_id if unique_id field not present, otherwise just retrieve from config_entry : ConfigEntry --- custom_components/uponor/__init__.py | 4 ++-- custom_components/uponor/climate.py | 6 ++---- custom_components/uponor/config_flow.py | 5 ++--- custom_components/uponor/helper.py | 12 +++++------- custom_components/uponor/switch.py | 4 ++-- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/custom_components/uponor/__init__.py b/custom_components/uponor/__init__.py index aad688f..4a047f2 100644 --- a/custom_components/uponor/__init__.py +++ b/custom_components/uponor/__init__.py @@ -38,7 +38,7 @@ ) from .helper import ( - get_unique_id_from + get_unique_id_from_config_entry ) _LOGGER = logging.getLogger(__name__) @@ -53,7 +53,7 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): host = config_entry.data[CONF_HOST] - unique_id = get_unique_id_from(config_entry.data[CONF_NAME]) + unique_id = get_unique_id_from_config_entry(config_entry) store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) state_proxy = await hass.async_add_executor_job(lambda: UponorStateProxy(hass, host, store, unique_id)) diff --git a/custom_components/uponor/climate.py b/custom_components/uponor/climate.py index cc50e59..943eaac 100644 --- a/custom_components/uponor/climate.py +++ b/custom_components/uponor/climate.py @@ -28,17 +28,15 @@ DEVICE_MANUFACTURER ) -from homeassistant.const import CONF_NAME - from .helper import ( - get_unique_id_from + get_unique_id_from_config_entry ) _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): - unique_id = get_unique_id_from(entry.data[CONF_NAME]) + unique_id = get_unique_id_from_config_entry(entry) state_proxy = hass.data[unique_id]["state_proxy"] diff --git a/custom_components/uponor/config_flow.py b/custom_components/uponor/config_flow.py index eae5ed6..b09a39b 100644 --- a/custom_components/uponor/config_flow.py +++ b/custom_components/uponor/config_flow.py @@ -19,8 +19,7 @@ from .helper import ( create_unique_id_from_user_input, - generate_unique_id_from_user_input, - get_unique_id_from + generate_unique_id_from_user_input_conf_name, ) _LOGGER = logging.getLogger(__name__) @@ -46,7 +45,7 @@ async def async_step_user(self, user_input=None): if user_input is not None: unique_id = create_unique_id_from_user_input(user_input) if unique_id is None: - unique_id = generate_unique_id_from_user_input(user_input) + unique_id = generate_unique_id_from_user_input_conf_name(user_input) await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() diff --git a/custom_components/uponor/helper.py b/custom_components/uponor/helper.py index 145e912..15b638a 100644 --- a/custom_components/uponor/helper.py +++ b/custom_components/uponor/helper.py @@ -3,6 +3,8 @@ CONF_UNIQUE_ID ) +from homeassistant.config_entries import ConfigEntry + from homeassistant.const import ( CONF_NAME ) @@ -15,16 +17,12 @@ def create_unique_id_from_user_input(user_input): return None -def generate_unique_id_from_user_input(user_input): +def generate_unique_id_from_user_input_conf_name(user_input): conf_name = user_input[CONF_NAME] raw_unique_id = DOMAIN + "_" + conf_name cleaned_unique_id = raw_unique_id.replace(" ", "_").lower() return cleaned_unique_id -def get_unique_id_from(conf_name: str): - - raw_unique_id = DOMAIN + "_" + conf_name - cleaned_unique_id = raw_unique_id.replace(" ", "_").lower() - - return cleaned_unique_id +def get_unique_id_from_config_entry(config_entry : ConfigEntry): + return config_entry.unique_id diff --git a/custom_components/uponor/switch.py b/custom_components/uponor/switch.py index 616c9d1..b195ee0 100644 --- a/custom_components/uponor/switch.py +++ b/custom_components/uponor/switch.py @@ -9,12 +9,12 @@ ) from .helper import ( - get_unique_id_from + get_unique_id_from_config_entry ) async def async_setup_entry(hass, entry, async_add_entities): - unique_id = get_unique_id_from(entry.data[CONF_NAME]) + unique_id = get_unique_id_from_config_entry(entry) state_proxy = hass.data[unique_id]["state_proxy"] entities = [AwaySwitch(unique_id, state_proxy, entry.data[CONF_NAME])] From 242f6c0131f4d0ca2d6e8be4b559fe405b39bca1 Mon Sep 17 00:00:00 2001 From: "Sietse T. Au" Date: Wed, 24 Jan 2024 22:00:34 +0100 Subject: [PATCH 4/4] remove unused imports --- custom_components/uponor/__init__.py | 9 +-------- custom_components/uponor/config_flow.py | 2 -- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/custom_components/uponor/__init__.py b/custom_components/uponor/__init__.py index 4a047f2..df90876 100644 --- a/custom_components/uponor/__init__.py +++ b/custom_components/uponor/__init__.py @@ -1,19 +1,12 @@ -from datetime import timedelta import math -import ipaddress -import requests -import voluptuous as vol import logging from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.const import CONF_HOST from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from UponorJnap import UponorJnap from .const import ( diff --git a/custom_components/uponor/config_flow.py b/custom_components/uponor/config_flow.py index b09a39b..4929e53 100644 --- a/custom_components/uponor/config_flow.py +++ b/custom_components/uponor/config_flow.py @@ -1,6 +1,5 @@ from homeassistant import config_entries import voluptuous as vol -import homeassistant.helpers.config_validation as cv import logging from UponorJnap import UponorJnap @@ -13,7 +12,6 @@ from .const import ( DOMAIN, CONF_UNIQUE_ID, - SIGNAL_UPONOR_STATE_UPDATE, DEVICE_MANUFACTURER )