From eb45b30d107d264a812c831a849ead3c461226b6 Mon Sep 17 00:00:00 2001 From: Seb Ruiz Date: Sun, 29 May 2022 20:33:25 +1000 Subject: [PATCH] Restore service to 3.0.0. Fixes #135 --- custom_components/bhyve/pybhyve/client.py | 34 ++++++++++ custom_components/bhyve/services.yaml | 10 +++ custom_components/bhyve/switch.py | 77 +++++++++++++++++++++-- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/custom_components/bhyve/pybhyve/client.py b/custom_components/bhyve/pybhyve/client.py index 948703e..611a26b 100644 --- a/custom_components/bhyve/pybhyve/client.py +++ b/custom_components/bhyve/pybhyve/client.py @@ -13,6 +13,7 @@ API_POLL_PERIOD, DEVICE_HISTORY_PATH, DEVICES_PATH, + LANDSCAPE_DESCRIPTIONS_PATH, LOGIN_PATH, TIMER_PROGRAMS_PATH, WS_HOST, @@ -45,6 +46,9 @@ def __init__(self, username: str, password: str, session) -> None: self._device_histories = {} self._last_poll_device_histories = 0 + self._landscapes = list[Any] + self._last_poll_landscapes = 0 + async def _request( self, method: str, endpoint: str, params: dict = None, json: dict = None ) -> list: @@ -121,6 +125,21 @@ async def _refresh_device_history(self, device_id, force_update=False): self._last_poll_device_histories = now + async def _refresh_landscapes(self, device_id, force_update=False): + now = time.time() + if force_update: + _LOGGER.debug("Forcing landscape refresh %s", device_id) + elif now - self._last_poll_landscapes < API_POLL_PERIOD: + return + + self._landscapes = await self._request( + "get", + f"{LANDSCAPE_DESCRIPTIONS_PATH}/{device_id}", + params={"t": str(time.time())}, + ) + + self._last_poll_landscapes = now + async def _async_ws_handler(self, async_callback, data): """Process incoming websocket message.""" ensure_future(async_callback(data)) @@ -193,6 +212,21 @@ async def get_device_history(self, device_id, force_update=False): await self._refresh_device_history(device_id, force_update=force_update) return self._device_histories.get(device_id) + async def get_landscape(self, device_id, zone_id, force_update=False): + """Get landscape by zone id.""" + await self._refresh_landscapes(device_id, force_update) + for zone in self._landscapes: + if zone.get("station") == zone_id: + return zone + return None + + async def update_landscape(self, landscape): + """Update the state of a zone landscape.""" + landscape_id = landscape.get("id") + path = f"{LANDSCAPE_DESCRIPTIONS_PATH}/{landscape_id}" + json = {"landscape_description": landscape} + await self._request("put", path, json=json) + async def update_program(self, program_id, program): """Update the state of a program.""" path = f"{TIMER_PROGRAMS_PATH}/{program_id}" diff --git a/custom_components/bhyve/services.yaml b/custom_components/bhyve/services.yaml index 8cd9d65..f0cf369 100644 --- a/custom_components/bhyve/services.yaml +++ b/custom_components/bhyve/services.yaml @@ -43,3 +43,13 @@ set_manual_preset_runtime: minutes: description: Number of minutes to set the preset runtime example: 15 + +set_smart_watering_soil_moisture: + description: Set the smart watering soil moisture level for a zone + fields: + entity_id: + description: Zone + example: "switch.backyard_zone" + percentage: + description: Moisture level between 0 - 100 (percent) + example: 50 diff --git a/custom_components/bhyve/switch.py b/custom_components/bhyve/switch.py index 8c05989..374613b 100644 --- a/custom_components/bhyve/switch.py +++ b/custom_components/bhyve/switch.py @@ -55,6 +55,7 @@ # Service Attributes ATTR_MINUTES = "minutes" ATTR_HOURS = "hours" +ATTR_PERCENTAGE = "percentage" # Rain Delay Attributes ATTR_CAUSE = "cause" @@ -88,11 +89,19 @@ } ) +SET_SMART_WATERING_SOIL_MOISTURE_SCHEMA = SERVICE_BASE_SCHEMA.extend( + { + vol.Required(ATTR_PERCENTAGE): cv.positive_int, + } +) + SERVICE_ENABLE_RAIN_DELAY = "enable_rain_delay" SERVICE_DISABLE_RAIN_DELAY = "disable_rain_delay" SERVICE_START_WATERING = "start_watering" SERVICE_STOP_WATERING = "stop_watering" SERVICE_SET_MANUAL_PRESET_RUNTIME = "set_manual_preset_runtime" +SERVICE_SET_SMART_WATERING_SOIL_MOISTURE = "set_smart_watering_soil_moisture" + SERVICE_TO_METHOD = { SERVICE_ENABLE_RAIN_DELAY: { @@ -112,6 +121,10 @@ "method": "set_manual_preset_runtime", "schema": SET_PRESET_RUNTIME_SCHEMA, }, + SERVICE_SET_SMART_WATERING_SOIL_MOISTURE: { + "method": "set_smart_watering_soil_moisture", + "schema": SET_SMART_WATERING_SOIL_MOISTURE_SCHEMA, + }, } @@ -316,6 +329,7 @@ def __init__(self, hass, bhyve, device, zone, device_programs, icon): self._zone_id = zone.get("station") self._entity_picture = zone.get("image_url") self._zone_name = zone.get("name") + self._smart_watering_enabled = zone.get("smart_watering_enabled") self._manual_preset_runtime = device.get( "manual_preset_runtime_sec", DEFAULT_MANUAL_RUNTIME.seconds ) @@ -333,7 +347,7 @@ def _setup(self, device): "device_name": self._device_name, "device_id": self._device_id, "zone_name": self._zone_name, - ATTR_SMART_WATERING_ENABLED: False, + ATTR_SMART_WATERING_ENABLED: self._smart_watering_enabled, } self._available = device.get("is_connected", False) @@ -407,9 +421,6 @@ def _set_watering_program(self, program): "is_smart_program": is_smart_program, } - if is_smart_program: - self._attrs[ATTR_SMART_WATERING_ENABLED] = program_enabled - if not program_enabled or not active_program_run_times: _LOGGER.info( "%s Zone: Watering program %s (%s) is not enabled, skipping", @@ -534,6 +545,64 @@ def is_on(self): """Return the status of the sensor.""" return self._is_on + async def set_smart_watering_soil_moisture(self, percentage): + """Set the soil moisture percentage for the zone.""" + if self._smart_watering_enabled: + landscape = None + try: + landscape = await self._bhyve.get_landscape( + self._device_id, self._zone_id + ) + + except BHyveError as err: + _LOGGER.warning( + "Unable to retreive current soil data for %s: %s", self.name, err + ) + + if landscape is not None: + _LOGGER.debug("Landscape data %s", landscape) + + # Define the minimum landscape update json payload + landscape_update = { + "current_water_level": 0, + "device_id": "", + "id": "", + "station": 0, + } + + # B-hyve computed value for 0% moisture + landscape_moisture_level_0 = landscape["replenishment_point"] + + # B-hyve computed value for 100% moisture + landscape_moisture_level_100 = landscape["field_capacity_depth"] + + # Set property to computed user desired soil moisture level + landscape_update["current_water_level"] = landscape_moisture_level_0 + ( + ( + percentage + * (landscape_moisture_level_100 - landscape_moisture_level_0) + ) + / 100.0 + ) + # Set remaining properties + landscape_update["device_id"] = self._device_id + landscape_update["id"] = landscape["id"] + landscape_update["station"] = self._zone_id + + try: + _LOGGER.debug("Landscape update %s", landscape_update) + await self._bhyve.update_landscape(landscape_update) + + except BHyveError as err: + _LOGGER.warning( + "Unable to set soil moisture level for %s: %s", self.name, err + ) + else: + _LOGGER.info( + "Zone %s isn't smart watering enabled, cannot set soil moisture.", + self._zone_name, + ) + async def start_watering(self, minutes): """Turns on the switch and starts watering.""" station_payload = [{"station": self._zone_id, "run_time": minutes}]