From 894d582f275265282ce4087c542832abe0c0866d Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 9 Jan 2024 09:38:39 +1100 Subject: [PATCH 1/2] Add valve wrapper entities for existing switch --- custom_components/linktap/const.py | 2 +- custom_components/linktap/services.yaml | 15 +- custom_components/linktap/valve.py | 198 ++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 custom_components/linktap/valve.py diff --git a/custom_components/linktap/const.py b/custom_components/linktap/const.py index 9a00070..155c9a0 100644 --- a/custom_components/linktap/const.py +++ b/custom_components/linktap/const.py @@ -12,7 +12,7 @@ DEFAULT_TIME = 15 DEFAULT_NAME = "Linktap Local Integration" DEFAULT_VOL = 0 -PLATFORMS = ['number', 'binary_sensor', 'sensor', 'switch'] +PLATFORMS = ['number', 'binary_sensor', 'sensor', 'switch', 'valve'] ATTR_DEFAULT_TIME = 'Default Time' ATTR_VOL = "Watering by Volume" ATTR_DURATION = "Watering Duration" diff --git a/custom_components/linktap/services.yaml b/custom_components/linktap/services.yaml index 2595bfd..b610604 100644 --- a/custom_components/linktap/services.yaml +++ b/custom_components/linktap/services.yaml @@ -17,7 +17,7 @@ dismiss_alerts: pause: name: Pause - description: Pause the watering + description: Pause the watering schedule fields: hours: name: Hours @@ -27,3 +27,16 @@ pause: entity: integration: linktap domain: switch + +pause_valve: + name: Pause + description: Pause the watering schedule + fields: + hours: + name: Hours + example: 1 + description: Duration in hours + target: + entity: + integration: linktap + domain: valve diff --git a/custom_components/linktap/valve.py b/custom_components/linktap/valve.py new file mode 100644 index 0000000..af9ebea --- /dev/null +++ b/custom_components/linktap/valve.py @@ -0,0 +1,198 @@ +import asyncio +import json +import logging +import random + +import aiohttp +import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.valve import ValveEntity, ValveEntityFeature +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import callback +from homeassistant.helpers import entity_platform, service, entity_registry as er +from homeassistant.helpers.entity import * +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import EventStateChangedData +from homeassistant.helpers.typing import EventType +from homeassistant.helpers.update_coordinator import (CoordinatorEntity, + DataUpdateCoordinator) +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +from .const import (ATTR_STATE, DOMAIN, GW_IP, + MANUFACTURER, NAME, TAP_ID) + + +async def async_setup_entry( + hass, config, async_add_entities, discovery_info=None +): + """Initialize Valve """ + taps = hass.data[DOMAIN][config.entry_id]["conf"]["taps"] + valves = [] + for tap in taps: + coordinator = tap["coordinator"] + _LOGGER.debug(f"Configuring valve for tap {tap}") + valves.append(LinktapValve(coordinator, hass, tap)) + async_add_entities(valves, True) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service("pause_valve", + {vol.Required("hours", default=1): vol.Coerce(int)}, + "_pause_tap" + ) + +class LinktapValve(CoordinatorEntity, ValveEntity): + def __init__(self, coordinator: DataUpdateCoordinator, hass, tap): + super().__init__(coordinator) + self._state = None + self._name = tap[NAME] + #self._id = tap[TAP_ID] + self.tap_id = tap[TAP_ID] + #self.tap_api = coordinator.tap_api + self.platform = "valve" + self.hass = hass + self._attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + self._attr_reports_position = False + self._attr_unique_id = slugify(f"{DOMAIN}_{self.platform}_{self.tap_id}") + #self._attr_icon = "mdi:water-pump" + self._attrs = { + "data": self.coordinator.data, + "switch": self.switch_entity, + # "duration_entity": self.duration_entity, + # "volume_entity": self.volume_entity + } + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, tap[TAP_ID]) + }, + name=tap[NAME], + manufacturer=MANUFACTURER, + configuration_url="http://" + tap[GW_IP] + "/" + ) + + @property + def unique_id(self): + return self._attr_unique_id + + @property + def name(self): + return f"{MANUFACTURER} {self._name}" + + @property + def switch_entity(self): + name = self._name.replace(" ", "_") + name = name.replace("-", "_") + return f"switch.{DOMAIN}_{name}".lower() + +# @property +# def duration_entity(self): +# name = self._name.replace(" ", "_") +# name = name.replace("-", "_") +# return f"number.{DOMAIN}_{name}_watering_duration".lower() + +# @property +# def volume_entity(self): +# name = self._name.replace(" ", "_") +# name = name.replace("-", "_") +# return f"number.{DOMAIN}_{name}_watering_volume".lower() + + async def async_open_valve(self, **kwargs): + #duration = self.get_watering_duration() + #seconds = int(float(duration)) * 60 + #volume = self.get_watering_volume() + #watering_volume = None + #if volume != DEFAULT_VOL: + # watering_volume = volume + #gw_id = self.coordinator.get_gw_id() + #attributes = await self.tap_api.turn_on(gw_id, self.tap_id, seconds, self.get_watering_volume()) + """Open the valve.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self.switch_entity}, + blocking=True, + context=self._context, + ) + await self.coordinator.async_request_refresh() + #self.control_result = await self.set_state(go="open") + #self.async_write_ha_state() + + async def async_close_valve(self, **kwargs): + """Close valve.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self.switch_entity}, + blocking=True, + context=self._context, + ) + await self.coordinator.async_request_refresh() + #self.control_result = await self.set_state(go="close") + #self.async_write_ha_state() + + @property + def extra_state_attributes(self): + return self._attrs + + @callback + def async_state_changed_listener( + self, event: EventType[EventStateChangedData] | None = None + ) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self.switch_entity)) is None + ): + return + + self._attr_is_closed = self._attrs[ATTR_STATE] != STATE_ON + + @property + def state(self): + status = self.coordinator.data + #self._attrs["data"] = status + #_LOGGER.debug(f"Switch Status: {status}") + #duration = self.get_watering_duration() + #_LOGGER.debug(f"Set duration:{duration}") + #volume = self.get_watering_volume() + #_LOGGER.debug(f"Set volume:{volume}") + self._attrs[ATTR_STATE] = status[ATTR_STATE] + state = "unknown" + if status[ATTR_STATE]: + state = "open" + elif not status[ATTR_STATE]: + state = "closed" + _LOGGER.debug(f"Valve {self.name} state {state}") + self._attr_is_closed = state != "open" + return state + + @property + def is_closed(self): + return self.state() == "closed" + + @property + def is_open(self): + return self.state() == "open" + + @property + def device_info(self) -> DeviceInfo: + return self._attr_device_info + + async def _pause_tap(self, hours=False): + if not hours: + hours = 1 + # Currently hard coding 1 hour for testing + _LOGGER.debug(f"Pausing {self.entity_id} for {hours} hours") + await self.tap_api.pause_tap(self._gw_id, self.tap_id, hours) + await self.coordinator.async_request_refresh() From f122b5d05181b6d48321c1f11addcb45631e279d Mon Sep 17 00:00:00 2001 From: Dan Wheaton Date: Tue, 9 Jan 2024 09:42:08 +1100 Subject: [PATCH 2/2] Updated required version in HACS --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index a5370b0..c575eaa 100644 --- a/hacs.json +++ b/hacs.json @@ -1,4 +1,4 @@ { "name": "Linktap Local", - "homeassistant": "2023.06.0" + "homeassistant": "2024.1.0" }