diff --git a/custom_components/bhyve/__init__.py b/custom_components/bhyve/__init__.py index 9c10c92..1f6eb43 100644 --- a/custom_components/bhyve/__init__.py +++ b/custom_components/bhyve/__init__.py @@ -15,7 +15,7 @@ ConfigEntryNotReady, HomeAssistantError, ) -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -33,7 +33,7 @@ SIGNAL_UPDATE_DEVICE, SIGNAL_UPDATE_PROGRAM, ) -from .util import filter_configured_devices +from .util import filter_configured_devices, constant_program_id from .pybhyve import Client from .pybhyve.errors import AuthenticationError, BHyveError @@ -82,6 +82,11 @@ async def async_update_callback(data): if event == EVENT_PROGRAM_CHANGED: device_id = data.get("program", {}).get("device_id") program_id = data.get("program", {}).get("id") + # Use a constant id if Smart program. + is_smart_program = bool( + data.get("program", {}).get("is_smart_program", False) + ) + program_id = constant_program_id(device_id, program_id, is_smart_program) else: device_id = data.get("device_id") @@ -142,6 +147,32 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating Bhyve from version %s", entry.version) + version = entry.version + device_id = entry.options["devices"][0] # Use ID of first device + + # Migrate Smart Watering program switch to new constant Unique ID + if version == 1: + registry = entity_registry.async_get(hass) + for entity_id, e_entry in registry.entities.items(): + if e_entry.config_entry_id == entry.entry_id: + new_unique_id = f"bhyve:{device_id}:program:smart_program" + if entity_id.endswith( + "_smart_watering_program" + ): # Only migrate the first switch + registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + entry.version = 2 + _LOGGER.info( + "Bhyve unique identifier for entity: %s has been updated", + entity_id, + ) + + _LOGGER.info("Migration to version %s successful", entry.version) + return True + + class BHyveEntity(Entity): """Define a base BHyve entity.""" diff --git a/custom_components/bhyve/config_flow.py b/custom_components/bhyve/config_flow.py index 6a5b149..e80074e 100644 --- a/custom_components/bhyve/config_flow.py +++ b/custom_components/bhyve/config_flow.py @@ -23,7 +23,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for BHyve.""" - VERSION = 1 + VERSION = 2 def __init__(self): """Initialize the config flow.""" diff --git a/custom_components/bhyve/switch.py b/custom_components/bhyve/switch.py index 374613b..f14f356 100644 --- a/custom_components/bhyve/switch.py +++ b/custom_components/bhyve/switch.py @@ -36,7 +36,11 @@ SIGNAL_UPDATE_PROGRAM, ) from .pybhyve.errors import BHyveError -from .util import filter_configured_devices, orbit_time_to_local_time +from .util import ( + filter_configured_devices, + orbit_time_to_local_time, + constant_program_id, +) _LOGGER = logging.getLogger(__name__) @@ -232,6 +236,7 @@ def __init__(self, hass, bhyve, device, program, icon): self._device_id = program.get("device_id") self._program_id = program.get("id") self._available = True + self._is_smart_program = bool(self._program.get("is_smart_program", False)) @property def extra_state_attributes(self): @@ -239,7 +244,7 @@ def extra_state_attributes(self): attrs = { "device_id": self._device_id, - "is_smart_program": self._program.get("is_smart_program", False), + "is_smart_program": self._is_smart_program, "frequency": self._program.get("frequency"), "start_times": self._program.get("start_times"), "budget": self._program.get("budget"), @@ -256,8 +261,8 @@ def is_on(self): @property def unique_id(self): - """Return the unique id for the switch program.""" - return f"bhyve:program:{self._program_id}" + """Return the unique id, unchanging string that represents this switch program.""" + return f"bhyve:{self._device_id}:program:{'smart_program' if self._is_smart_program else self._program_id}" @property def entity_category(self): @@ -290,8 +295,13 @@ def update(device_id, data): self._ws_unprocessed_events.append(data) self.async_schedule_update_ha_state(True) + # Use a constant id so that it is updated on change. + program_id = constant_program_id( + self._device_id, self._program_id, self._is_smart_program + ) + self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_PROGRAM.format(self._program_id), update + self.hass, SIGNAL_UPDATE_PROGRAM.format(program_id), update ) async def async_will_remove_from_hass(self): @@ -314,6 +324,8 @@ def _on_ws_data(self, data): program = data.get("program") if program is not None: self._program = program + # Update Smart Watering program_id to match id if changed on irrigation system. + self._program_id = program.get("id") def _should_handle_event(self, event_name, data): return event_name in [EVENT_PROGRAM_CHANGED] diff --git a/custom_components/bhyve/util.py b/custom_components/bhyve/util.py index a12d90a..da33dac 100755 --- a/custom_components/bhyve/util.py +++ b/custom_components/bhyve/util.py @@ -1,3 +1,4 @@ +import hashlib from homeassistant.config_entries import ConfigEntry from homeassistant.util import dt @@ -14,3 +15,13 @@ def orbit_time_to_local_time(timestamp: str): def filter_configured_devices(entry: ConfigEntry, all_devices): """Filter the device list to those that are enabled in options.""" return [d for d in all_devices if str(d["id"]) in entry.options[CONF_DEVICES]] + + +def constant_program_id(device_id, program_id, is_smart_program: bool = False): + """For devices with multiple zones, Smart program id changes depending on the zone/s that are included. + Generate a constant id so that it is updated on change.""" + if is_smart_program: + program_id = hashlib.md5( + "{}:smart_program".format(device_id).encode("utf-8") + ).hexdigest() + return program_id