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:
parent
7ab48d968e
commit
3e9fe3e06f
@ -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
|
||||
|
12
custom_components/badnest/services.yaml
Normal file
12
custom_components/badnest/services.yaml
Normal 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
|
267
custom_components/badnest/water_heater.py
Normal file
267
custom_components/badnest/water_heater.py
Normal 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()
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "Bad Nest",
|
||||
"domains": ["climate", "camera", "sensor"]
|
||||
"domains": ["climate", "camera", "sensor", "water_heater"]
|
||||
}
|
||||
|
7
info.md
7
info.md
@ -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/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user