1
0
mirror of https://github.com/USA-RedDragon/badnest.git synced 2025-03-13 19:27:50 +00:00

Initial commit of water_heater support for badnest

This commit allows the UK 3rd Generation Learning Thermostat, which has support
for controlling hot water heating.
Support is added for:
- Setting mode to Off or Schedule
- Setting Home/Away Assist Off or On
- Setting Boost mode On or Off (with thanks to the work done by @c-garner
  which this is based on)
- Status of hot water heating - is heating active True/False (Matches when the
  Nest app goes orange)
- Status of boost mode - True if boost is active
- Status of Away Mode Active - True if no one's been home for 48hrs
  and Home/Away Assist is On, which turns off hot water schedule
This commit is contained in:
Guy Badman 2020-04-22 17:56:16 +01:00
parent 7ab48d968e
commit 3e9fe3e06f
No known key found for this signature in database
GPG Key ID: 0259B610971EA10F
5 changed files with 391 additions and 1 deletions

View File

@ -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,25 @@ class NestAPI():
self.device_data[sn]['eco'] = True
else:
self.device_data[sn]['eco'] = False
# Hot water
# - Status
self.device_data[sn]['has_hot_water_control'] = \
sensor_data["has_hot_water_control"]
self.device_data[sn]['hot_water_status'] = \
sensor_data["hot_water_active"]
self.device_data[sn]['hot_water_actively_heating'] = \
sensor_data["hot_water_boiling_state"]
self.device_data[sn]['hot_water_away_active'] = \
sensor_data["hot_water_away_active"]
# - Status/Settings
self.device_data[sn]['hot_water_timer_mode'] = \
sensor_data["hot_water_mode"]
self.device_data[sn]['hot_water_away_setting'] = \
sensor_data["hot_water_away_enabled"]
self.device_data[sn]['hot_water_boost_setting'] = \
sensor_data["hot_water_boost_time_to_end"]
# Protect
elif bucket["object_key"].startswith(
f"topaz.{sn}"):
@ -484,6 +505,89 @@ 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 hotwater_set_away_mode(self, device_id, away_mode):
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_away_enabled": away_mode},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to set hot water away mode, trying again')
self.hotwater_set_away_mode(device_id, away_mode)
except KeyError:
_LOGGER.debug('Failed to set hot water away mode, '
'trying to log in again')
self.login()
self.hotwater_set_away_mode(device_id, away_mode)
def hotwater_set_mode(self, device_id, mode):
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_mode": mode},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to set hot water mode, trying again')
self.hotwater_set_boost(device_id, mode)
except KeyError:
_LOGGER.debug('Failed to set hot water mode, '
'trying to log in again')
self.login()
self.hotwater_set_boost(device_id, mode)
def _camera_set_properties(self, device_id, property, value):
if device_id not in self.cameras:
return

View File

@ -0,0 +1,12 @@
boost_hot_water:
description: Turn boost mode on for the period of time specified, or off."
fields:
entity_id:
description: Name(s) of entites to change.
example: "water_heater.hot_water"
time_period:
description: Time period in minutes for the boost.
example: 30
boost_mode:
description: Enable boost funcion.
example: True

View File

@ -0,0 +1,267 @@
import logging
import time
import voluptuous as vol
from datetime import datetime
from homeassistant.util.dt import utcnow
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID,
)
from homeassistant.components.water_heater import (
WaterHeaterDevice,
SCAN_INTERVAL,
STATE_OFF,
STATE_ON,
STATE_ECO,
SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE,
ATTR_AWAY_MODE,
ATTR_OPERATION_MODE,
ATTR_OPERATION_LIST,
)
from .const import (
DOMAIN,
)
STATE_SCHEDULE = 'schedule'
SERVICE_BOOST_HOT_WATER = 'boost_hot_water'
SUPPORT_BOOST_MODE = 8
ATTR_TIME_PERIOD = 'time_period'
ATTR_BOOST_MODE_STATUS = 'boost_mode_status'
ATTR_BOOST_MODE = 'boost_mode'
ATTR_HEATING_ACTIVE = 'heating_active'
ATTR_AWAY_MODE_ACTIVE = 'away_mode_active'
SUPPORTED_FEATURES = SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE | SUPPORT_BOOST_MODE
NEST_TO_HASS_MODE = {"schedule": STATE_SCHEDULE, "off": STATE_OFF}
HASS_TO_NEST_MODE = {STATE_SCHEDULE: "schedule", STATE_OFF: "off"}
NEST_TO_HASS_STATE = {True: STATE_ON, False: STATE_OFF}
HASS_TO_NEST_STATE = {STATE_ON: True, STATE_OFF: False}
SUPPORTED_OPERATIONS = [STATE_SCHEDULE, STATE_OFF]
BOOST_HOT_WATER_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Optional(ATTR_TIME_PERIOD, default=30): cv.positive_int,
vol.Required(ATTR_BOOST_MODE): cv.boolean,
}
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass,
config,
async_add_entities,
discovery_info=None):
"""Set up the Nest water heater device."""
api = hass.data[DOMAIN]['api']
waterheaters = []
_LOGGER.info("Adding waterheaters")
for waterheater in api['hotwatercontrollers']:
_LOGGER.info(f"Adding nest waterheater uuid: {waterheater}")
waterheaters.append(NestWaterHeater(waterheater, api))
async_add_entities(waterheaters)
def hot_water_boost(service):
"""Handle the service call."""
all_waterheaters = api['hotwatercontrollers']
entity_ids = service.data[ATTR_ENTITY_ID]
minutes = service.data[ATTR_TIME_PERIOD]
timeToEnd = int(time.mktime(datetime.timetuple(utcnow()))+(minutes*60))
mode = service.data[ATTR_BOOST_MODE]
_LOGGER.debug('HW boost mode: {} ending: {}'.format(mode, timeToEnd))
_waterheaters = [
x for x in waterheaters if not entity_ids or x.entity_id in entity_ids
]
for nest_water_heater in _waterheaters:
if mode:
_LOGGER.info('HW boost mode on time ending: {}'.format(timeToEnd))
nest_water_heater.turn_boost_mode_on(timeToEnd)
else:
_LOGGER.info('HW boost off')
nest_water_heater.turn_boost_mode_off()
hass.services.async_register(
DOMAIN,
SERVICE_BOOST_HOT_WATER,
hot_water_boost,
schema=BOOST_HOT_WATER_SCHEMA,
)
class NestWaterHeater(WaterHeaterDevice):
"""Representation of a Nest water heater device."""
def __init__(self, device_id, api):
"""Initialize the sensor."""
self._name = "Nest Hot Water Heater"
self.device_id = device_id
self.device = api
@property
def unique_id(self):
"""Return an unique ID."""
return self.device_id + '_hw'
@property
def device_info(self):
"""Return device information."""
return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name}
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORTED_FEATURES
@property
def name(self):
"""Return the name of the water heater."""
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" if self.current_operation == STATE_SCHEDULE else "mdi:water-off"
@property
def state(self):
""" Return the (master) state of the water heater."""
state = STATE_OFF
if self.device.device_data[self.device_id]['hot_water_status']:
state = NEST_TO_HASS_STATE[self.device.device_data[self.device_id]['hot_water_status']]
return state
@property
def capability_attributes(self):
"""Return capability attributes."""
supported_features = self.supported_features or 0
data = {}
if supported_features & SUPPORT_OPERATION_MODE:
data[ATTR_OPERATION_LIST] = self.operation_list
return data
@property
def state_attributes(self):
"""Return the optional state attributes."""
data = {}
supported_features = self.supported_features
# Operational mode will be off or schedule
if supported_features & SUPPORT_OPERATION_MODE:
data[ATTR_OPERATION_MODE] = self.current_operation
# This is, is the away mode feature turned on/off
if supported_features & SUPPORT_AWAY_MODE:
is_away = self.is_away_mode_on
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
# away_mode_active - true if away mode is active.
# If away mode is on, and no one has been seen for 48hrs away mode
# should go active
if supported_features & SUPPORT_AWAY_MODE:
if self.device.device_data[self.device_id]['hot_water_away_active']:
away_active = self.device.device_data[self.device_id]['hot_water_away_active']
data[ATTR_AWAY_MODE_ACTIVE] = away_active
else:
data[ATTR_AWAY_MODE_ACTIVE] = False
if supported_features & SUPPORT_BOOST_MODE:
# boost_mode - true if boost mode is currently active.
# boost_mode will be 0 if off, and non-zero otherwise - it is set to
# the epoch time when the boost is due to end
if self.device.device_data[self.device_id]['hot_water_boost_setting']:
boost_mode = self.device.device_data[self.device_id]['hot_water_boost_setting']
data[ATTR_BOOST_MODE_STATUS] = bool(boost_mode)
else:
data[ATTR_BOOST_MODE_STATUS] = False
# heating_active - true if hot water is currently being heated.
# So it is either on via schedule or boost and currently firing
# (demand on) the boiler
if self.device.device_data[self.device_id]['hot_water_actively_heating']:
boiler_firing = self.device.device_data[self.device_id]['hot_water_actively_heating']
data[ATTR_HEATING_ACTIVE] = boiler_firing
else:
data[ATTR_HEATING_ACTIVE] = False
_LOGGER.debug("Device state attributes: {}".format(data))
return data
@property
def current_operation(self):
"""Return current operation ie. eco, electric, performance, ..."""
return NEST_TO_HASS_MODE[self.device.device_data[self.device_id]['hot_water_timer_mode']]
@property
def operation_list(self):
"""Return the list of available operation modes."""
return SUPPORTED_OPERATIONS
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
away = self.device.device_data[self.device_id]['hot_water_away_setting']
return away
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
if self.device.device_data[self.device_id]['has_hot_water_control']:
self.device.hotwater_set_mode(self.device_id, mode=operation_mode)
async def async_set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
await self.hass.async_add_executor_job(self.set_operation_mode, operation_mode)
def turn_away_mode_on(self):
"""Turn away mode on."""
if self.device.device_data[self.device_id]['has_hot_water_control']:
self.device.hotwater_set_away_mode(self.device_id, away_mode=True)
async def async_turn_away_mode_on(self):
"""Turn away mode on."""
await self.hass.async_add_executor_job(self.turn_away_mode_on)
def turn_away_mode_off(self):
"""Turn away mode off."""
if self.device.device_data[self.device_id]['has_hot_water_control']:
self.device.hotwater_set_away_mode(self.device_id, away_mode=False)
async def async_turn_away_mode_off(self):
"""Turn away mode off."""
await self.hass.async_add_executor_job(self.turn_away_mode_off)
def turn_boost_mode_on(self, timeToEnd):
"""Turn boost mode on."""
if self.device.device_data[self.device_id]['has_hot_water_control']:
self.device.hotwater_set_boost(self.device_id, time=timeToEnd)
def turn_boost_mode_off(self):
"""Turn boost mode off."""
if self.device.device_data[self.device_id]['has_hot_water_control']:
self.device.hotwater_set_boost(self.device_id, time=0)
def update(self):
"""Get the latest data from the Hot Water Sensor and updates the states."""
self.device.update()
async def async_service_away_mode(entity, service):
"""Handle away mode service."""
if service.data[ATTR_AWAY_MODE]:
await entity.async_turn_away_mode_on()
else:
await entity.async_turn_away_mode_off()

View File

@ -1,4 +1,4 @@
{
"name": "Bad Nest",
"domains": ["climate", "camera", "sensor"]
"domains": ["climate", "camera", "sensor", "water_heater"]
}

View File

@ -13,6 +13,7 @@ This isn't an advertised or public API, it's still better than web scraping, but
- Nest Protect support
- Nest Thermostat support
- Nest Thermostat Sensor support
- Nest Thermostat Hot Water (water heater) support (UK)
- Nest Camera support
## Drawbacks
@ -52,6 +53,9 @@ camera:
sensor:
- platform: badnest
water_heater:
- platform: badnest
```
### Example configuration.yaml - When you are using the Google Auth Login
@ -71,6 +75,9 @@ camera:
sensor:
- platform: badnest
water_heater:
- platform: badnest
```
Google Login support added with many thanks to: chrisjshull from <https://github.com/chrisjshull/homebridge-nest/>