diff --git a/custom_components/badnest/__init__.py b/custom_components/badnest/__init__.py index 85f436f..a205342 100644 --- a/custom_components/badnest/__init__.py +++ b/custom_components/badnest/__init__.py @@ -1,10 +1,22 @@ """The example integration.""" +import logging import voluptuous as vol from homeassistant.helpers import config_validation as cv +from datetime import datetime +import time +from homeassistant.util.dt import utcnow + +from homeassistant.const import ( + ATTR_ENTITY_ID +) from .api import NestAPI from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_USER_ID, CONF_ACCESS_TOKEN, CONF_REGION +SERVICE_BOOST_HOT_WATER = "boost_hot_water" +ATTR_TIME_PERIOD = "time_period" +ATTR_MODE = "on_off" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -23,9 +35,36 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +BOOST_HOT_WATER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(ATTR_TIME_PERIOD, default=30): cv.positive_int, + vol.Required(ATTR_MODE): cv.string, + } +) + +_LOGGER = logging.getLogger(__name__) def setup(hass, config): """Set up the badnest component.""" + def hot_water_boost(service): + """Handle the service call.""" + node_id = hass.data.get(service.data[ATTR_ENTITY_ID]) + if not node_id: + # log or raise error + _LOGGER.error("Cannot boost entity id entered") + return + + minutes = service.data[ATTR_TIME_PERIOD] + timeToEnd = int(time.mktime(datetime.timetuple(utcnow()))+(minutes*60)) + + mode = service.data[ATTR_MODE] + + if mode == "on": + api.hotwater_set_boost(node_id, timeToEnd) + elif mode == "off": + api.hotwater_set_boost(node_id, 0) + if config.get(DOMAIN) is not None: user_id = config[DOMAIN].get(CONF_USER_ID) access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) @@ -49,4 +88,13 @@ def setup(hass, config): ), } + api = hass.data[DOMAIN]['api'] + + hass.services.register( + DOMAIN, + SERVICE_BOOST_HOT_WATER, + hot_water_boost, + schema=BOOST_HOT_WATER_SCHEMA, + ) + return True diff --git a/custom_components/badnest/api.py b/custom_components/badnest/api.py index 6695414..4fdfb91 100644 --- a/custom_components/badnest/api.py +++ b/custom_components/badnest/api.py @@ -48,6 +48,7 @@ class NestAPI(): self.cameras = [] self.thermostats = [] self.temperature_sensors = [] + self.hotwatercontrollers = [] self.protects = [] self.login() self._get_devices() @@ -153,6 +154,7 @@ class NestAPI(): sn = bucket.replace('device.', '') self.thermostats.append(sn) self.temperature_sensors.append(sn) + self.hotwatercontrollers.append(sn) self.device_data[sn] = {} self.cameras = self._get_cameras() @@ -270,6 +272,9 @@ class NestAPI(): self.device_data[sn]['eco'] = True else: self.device_data[sn]['eco'] = False + # Hot water + self.device_data[sn]['hot_water_active'] = \ + sensor_data["hot_water_active"] # Protect elif bucket["object_key"].startswith( f"topaz.{sn}"): @@ -484,6 +489,34 @@ class NestAPI(): self.login() self.thermostat_set_eco_mode(device_id, state) + def hotwater_set_boost(self, device_id, time): + if device_id not in self.hotwatercontrollers: + return + + try: + self._session.post( + f"{self._czfe_url}/v5/put", + json={ + "objects": [ + { + "object_key": f'device.{device_id}', + "op": "MERGE", + "value": {"hot_water_boost_time_to_end": time}, + } + ] + }, + headers={"Authorization": f"Basic {self._access_token}"}, + ) + except requests.exceptions.RequestException as e: + _LOGGER.error(e) + _LOGGER.error('Failed to boost hot water, trying again') + self.hotwater_set_boost(device_id, time) + except KeyError: + _LOGGER.debug('Failed to boost hot water, trying to log in again') + self.login() + self.hotwater_set_boost(device_id, time) + + def _camera_set_properties(self, device_id, property, value): if device_id not in self.cameras: return diff --git a/custom_components/badnest/climate.py b/custom_components/badnest/climate.py index e21f22d..12416bb 100644 --- a/custom_components/badnest/climate.py +++ b/custom_components/badnest/climate.py @@ -116,7 +116,10 @@ class NestClimate(ClimateDevice): if self.device.device_data[device_id]['target_humidity_enabled']: self._support_flags = self._support_flags | SUPPORT_TARGET_HUMIDITY - + + async def async_added_to_hass(self): + """When entity is added to Home Assistant.""" + self.hass.data[self.entity_id] = self.device_id @property def unique_id(self): @@ -157,7 +160,7 @@ class NestClimate(ClimateDevice): def target_humidity(self): """Return the target humidity.""" return self.device.device_data[self.device_id]['target_humidity'] - + @property def min_humidity(self): """Return the min target humidity.""" diff --git a/custom_components/badnest/sensor.py b/custom_components/badnest/sensor.py index 5638e78..534c3bc 100644 --- a/custom_components/badnest/sensor.py +++ b/custom_components/badnest/sensor.py @@ -32,7 +32,14 @@ async def async_setup_platform(hass, _LOGGER.info(f"Adding nest temp sensor uuid: {sensor}") temperature_sensors.append(NestTemperatureSensor(sensor, api)) + hw_sensors = [] + _LOGGER.info("Adding hot water sensors") + for hotwater in api['hotwatercontrollers']: + _LOGGER.info(f"Adding nest hot water sensor uuid: {hotwater}") + hw_sensors.append(NestHWSensor(hotwater, api)) + async_add_entities(temperature_sensors) + async_add_entities(hw_sensors) protect_sensors = [] _LOGGER.info("Adding protect sensors") @@ -92,6 +99,43 @@ class NestTemperatureSensor(Entity): } +class NestHWSensor(Entity): + """Implementation of the Nest Hot Water sensor.""" + + def __init__(self, device_id, api): + """Initialize the sensor.""" + self._name = "Nest Hot Water Sensor" + self.device_id = device_id + self.device = api + + @property + def unique_id(self): + """Return an unique ID.""" + return self.device_id + '_hw' + + @property + def name(self): + """Return the name of the sensor.""" + return "{0} Hot Water".format(self.device.device_data[self.device_id]['name']) + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:water" + + @property + def state(self): + """Return the state of the sensor.""" + if self.device.device_data[self.device_id]['hot_water_active']: + return 'On' + else: + return 'Off' + + def update(self): + """Get the latest data from the Protect and updates the states.""" + self.device.update() + + class NestProtectSensor(Entity): """Implementation of the Nest Protect sensor.""" diff --git a/custom_components/badnest/services.yaml b/custom_components/badnest/services.yaml new file mode 100644 index 0000000..1fb92f6 --- /dev/null +++ b/custom_components/badnest/services.yaml @@ -0,0 +1,11 @@ +boost_hot_water: + description: + "Set the boost mode ON or OFF defining the period of time for the boost." + fields: + entity_id: + { + description: Enter the entity_id for the device reuired to set the boost mode., + example: "water_heater.hot_water", + } + time_period: { description: Set the time period in minutes for the boost., example: 30} + on_off: { description: Set the boost function on or off., example: "on" } diff --git a/info.md b/info.md index 7e14cff..dfc9981 100644 --- a/info.md +++ b/info.md @@ -14,6 +14,7 @@ This isn't an advertised or public API, it's still better than web scraping, but - Nest Thermostat support - Nest Thermostat Sensor support - Nest Camera support +- Nest Hot Water support (UK) ## Drawbacks @@ -28,6 +29,14 @@ The camera's region is one of `us` or `eu` depending on your region. If you're not in the US or EU, you should be able to add your two-character country code, and it should work. +## Hot Water Control (UK) + +Hot water boosting is controlled through the `badnest.boost_hot_water` service. +The required variables are: + `entity_id` + `time_period` - integer in minutes + `on_off` + ### Example configuration.yaml - When you're not using the Google Auth Login Google recently introduced reCAPTCHA when logging to Nest. That means username