Okay, with a bit of time, a fresher head this morning and it snowing outside - I figured I’d try this again. Leaving out ChatGPT (it’s not always right!) I’ve got to the stage where Nest Thermostat, Nest Protect and Hot Water working again. I’ve checked the functionality, all looks good - but please let me know if you spot anything… hope this helps others.
climate.py
from datetime import datetime
import logging
try:
from homeassistant.components.climate import ClimateEntity, ClimateEntityFeature
except ImportError:
from homeassistant.components.climate import ClimateDevice as ClimateEntity
from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
FAN_AUTO,
FAN_ON,
HVACMode,
PRESET_ECO,
PRESET_NONE,
)
from homeassistant.const import (
ATTR_TEMPERATURE,
UnitOfTemperature,
)
from .const import (
DOMAIN,
)
NEST_MODE_HEAT_COOL = "range"
NEST_MODE_ECO = "eco"
NEST_MODE_HEAT = "heat"
NEST_MODE_COOL = "cool"
NEST_MODE_OFF = "off"
MODE_HASS_TO_NEST = {
HVACMode.HEAT_COOL: NEST_MODE_HEAT_COOL,
HVACMode.HEAT: NEST_MODE_HEAT,
HVACMode.COOL: NEST_MODE_COOL,
HVACMode.OFF: NEST_MODE_OFF,
}
ACTION_NEST_TO_HASS = {
"off": HVACMode.OFF,
"heating": HVACMode.HEAT,
"cooling": HVACMode.COOL,
}
MODE_NEST_TO_HASS = {v: k for k, v in MODE_HASS_TO_NEST.items()}
ROUND_TARGET_HUMIDITY_TO_NEAREST = 5
NEST_HUMIDITY_MIN = 10
NEST_HUMIDITY_MAX = 60
PRESET_MODES = [PRESET_NONE, PRESET_ECO]
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass,
config,
async_add_entities,
discovery_info=None):
"""Set up the Nest climate device."""
api = hass.data[DOMAIN]['api']
thermostats = []
_LOGGER.info("Adding thermostats")
for thermostat in api['thermostats']:
_LOGGER.info(f"Adding nest thermostat uuid: {thermostat}")
thermostats.append(NestClimate(thermostat, api))
async_add_entities(thermostats)
class NestClimate(ClimateEntity):
"""Representation of a Nest climate entity."""
def __init__(self, device_id, api):
"""Initialize the thermostat."""
self._name = "Nest Thermostat"
self._unit_of_measurement = UnitOfTemperature.CELSIUS
self._fan_modes = [FAN_ON, FAN_AUTO]
self.device_id = device_id
# Set the default supported features
self._support_flags = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
# Not all nest devices support cooling and heating remove unused
self._operation_list = []
self.device = api
if self.device.device_data[device_id]['can_heat'] \
and self.device.device_data[device_id]['can_cool']:
self._operation_list.append(HVACMode.HEAT_COOL)
self._support_flags |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
# Add supported nest thermostat features
if self.device.device_data[device_id]['can_heat']:
self._operation_list.append(HVACMode.HEAT)
if self.device.device_data[device_id]['can_cool']:
self._operation_list.append(HVACMode.COOL)
self._operation_list.append(HVACMode.OFF)
# feature of device
if self.device.device_data[device_id]['has_fan']:
self._support_flags = self._support_flags | ClimateEntityFeature.FAN_MODE
if self.device.device_data[device_id]['target_humidity_enabled']:
self._support_flags = self._support_flags | ClimateEntityFeature.TARGET_HUMIDITY
@property
def unique_id(self):
"""Return an unique ID."""
return self.device_id
@property
def name(self):
"""Return an friendly name."""
return self.device.device_data[self.device_id]['name']
@property
def supported_features(self):
"""Return the list of supported features."""
return self._support_flags
@property
def should_poll(self):
"""Return the polling state."""
return True
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
return self.device.device_data[self.device_id]['current_temperature']
@property
def current_humidity(self):
"""Return the current humidity."""
return self.device.device_data[self.device_id]['current_humidity']
@property
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."""
return NEST_HUMIDITY_MIN
@property
def max_humidity(self):
"""Return the max target humidity."""
return NEST_HUMIDITY_MAX
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self.device.device_data[self.device_id]['mode'] \
!= NEST_MODE_HEAT_COOL \
and not self.device.device_data[self.device_id]['eco']:
return \
self.device.device_data[self.device_id]['target_temperature']
return None
@property
def target_temperature_high(self):
"""Return the highbound target temperature we try to reach."""
if self.device.device_data[self.device_id]['mode'] \
== NEST_MODE_HEAT_COOL \
and not self.device.device_data[self.device_id]['eco']:
return \
self.device. \
device_data[self.device_id]['target_temperature_high']
return None
@property
def target_temperature_low(self):
"""Return the lowbound target temperature we try to reach."""
if self.device.device_data[self.device_id]['mode'] \
== NEST_MODE_HEAT_COOL \
and not self.device.device_data[self.device_id]['eco']:
return \
self.device. \
device_data[self.device_id]['target_temperature_low']
return None
@property
def hvac_action(self):
"""Return current operation ie. heat, cool, idle."""
return ACTION_NEST_TO_HASS[
self.device.device_data[self.device_id]['action']
]
@property
def hvac_mode(self):
"""Return hvac target hvac state."""
if self.device.device_data[self.device_id]['mode'] is None \
or self.device.device_data[self.device_id]['eco']:
# We assume the first operation in operation list is the main one
return self._operation_list[0]
return self.device.device_data[self.device_id]['mode']
@property
def hvac_modes(self):
"""Return the list of available operation modes."""
return self._operation_list
@property
def preset_mode(self):
"""Return current preset mode."""
if self.device.device_data[self.device_id]['eco']:
return PRESET_ECO
return PRESET_NONE
@property
def preset_modes(self):
"""Return preset modes."""
return PRESET_MODES
@property
def fan_mode(self):
"""Return whether the fan is on."""
if self.device.device_data[self.device_id]['has_fan']:
# Return whether the fan is on
if self.device.device_data[self.device_id]['fan']:
return FAN_ON
else:
return FAN_AUTO
# No Fan available so disable slider
return None
@property
def fan_modes(self):
"""Return the list of available fan modes."""
if self.device.device_data[self.device_id]['has_fan']:
return self._fan_modes
return None
def set_temperature(self, **kwargs):
"""Set new target temperature."""
temp = None
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if self.device.device_data[self.device_id]['mode'] == \
NEST_MODE_HEAT_COOL:
if target_temp_low is not None and target_temp_high is not None:
self.device.thermostat_set_temperature(
self.device_id,
target_temp_low,
target_temp_high,
)
else:
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is not None:
self.device.thermostat_set_temperature(
self.device_id,
temp,
)
def set_humidity(self, humidity):
"""Set new target humidity."""
humidity = int(round(float(humidity) / ROUND_TARGET_HUMIDITY_TO_NEAREST) * ROUND_TARGET_HUMIDITY_TO_NEAREST)
if humidity < NEST_HUMIDITY_MIN:
humidity = NEST_HUMIDITY_MIN
if humidity > NEST_HUMIDITY_MAX:
humidity = NEST_HUMIDITY_MAX
self.device.thermostat_set_target_humidity(
self.device_id,
humidity,
)
def set_hvac_mode(self, hvac_mode):
"""Set operation mode."""
self.device.thermostat_set_mode(
self.device_id,
MODE_HASS_TO_NEST[hvac_mode],
)
def set_fan_mode(self, fan_mode):
"""Turn fan on/off."""
if self.device.device_data[self.device_id]['has_fan']:
if fan_mode == "on":
self.device.thermostat_set_fan(
self.device_id,
int(datetime.now().timestamp() + 60 * 30),
)
else:
self.device.thermostat_set_fan(
self.device_id,
0,
)
def set_preset_mode(self, preset_mode):
"""Set preset mode."""
need_eco = preset_mode == PRESET_ECO
if need_eco != self.device.device_data[self.device_id]['eco']:
self.device.thermostat_set_eco_mode(
self.device_id,
need_eco,
)
def update(self):
"""Updates data."""
self.device.update()
sensor.py
import logging
from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import SensorEntity, SensorDeviceClass
from .const import DOMAIN
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
UnitOfTemperature
)
_LOGGER = logging.getLogger(__name__)
PROTECT_SENSOR_TYPES = [
"co_status",
"smoke_status",
"battery_health_state"
]
async def async_setup_platform(hass,
config,
async_add_entities,
discovery_info=None):
"""Set up the Nest climate device."""
api = hass.data[DOMAIN]['api']
temperature_sensors = []
_LOGGER.info("Adding temperature sensors")
for sensor in api['temperature_sensors']:
_LOGGER.info(f"Adding nest temp sensor uuid: {sensor}")
temperature_sensors.append(NestTemperatureSensor(sensor, api))
async_add_entities(temperature_sensors)
protect_sensors = []
_LOGGER.info("Adding protect sensors")
for sensor in api['protects']:
_LOGGER.info(f"Adding nest protect sensor uuid: {sensor}")
for sensor_type in PROTECT_SENSOR_TYPES:
protect_sensors.append(NestProtectSensor(sensor, sensor_type, api))
async_add_entities(protect_sensors)
class NestTemperatureSensor(Entity):
"""Implementation of the Nest Temperature Sensor."""
def __init__(self, device_id, api):
"""Initialize the sensor."""
self._name = "Nest Temperature Sensor"
self._unit_of_measurement = UnitOfTemperature.CELSIUS
self.device_id = device_id
self.device = api
@property
def unique_id(self):
"""Return an unique ID."""
return self.device_id
@property
def name(self):
"""Return the name of the sensor."""
return self.device.device_data[self.device_id]['name']
@property
def state(self):
"""Return the state of the sensor."""
return self.device.device_data[self.device_id]['temperature']
@property
def device_class(self):
"""Return the device class of this entity."""
return SensorDeviceClass.TEMPERATURE
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
def update(self):
"""Get the latest data from the DHT and updates the states."""
self.device.update()
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_BATTERY_LEVEL:
self.device.device_data[self.device_id]['battery_level']
}
class NestProtectSensor(Entity):
"""Implementation of the Nest Protect sensor."""
def __init__(self, device_id, sensor_type, api):
"""Initialize the sensor."""
self._name = "Nest Protect Sensor"
self.device_id = device_id
self._sensor_type = sensor_type
self.device = api
@property
def unique_id(self):
"""Return an unique ID."""
return self.device_id + '_' + self._sensor_type
@property
def name(self):
"""Return the name of the sensor."""
return self.device.device_data[self.device_id]['name'] + \
f' {self._sensor_type}'
@property
def state(self):
"""Return the state of the sensor."""
return self.device.device_data[self.device_id][self._sensor_type]
def update(self):
"""Get the latest data from the Protect and updates the states."""
self.device.update()
water_heater.py
import logging
import time
import voluptuous as vol
from datetime import datetime
from homeassistant.util.dt import now
from homeassistant.helpers import config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID,
UnitOfTemperature
)
from homeassistant.components.water_heater import (
STATE_OFF,
STATE_ON,
WaterHeaterEntity,
WaterHeaterEntityFeature,
ATTR_OPERATION_MODE,
ATTR_AWAY_MODE
)
from .const import (
DOMAIN,
)
STATE_SCHEDULE = 'schedule'
SERVICE_BOOST_HOT_WATER = 'boost_hot_water'
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 = (
WaterHeaterEntityFeature.OPERATION_MODE |
WaterHeaterEntityFeature.AWAY_MODE |
WaterHeaterEntityFeature.ON_OFF
)
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."""
entity_ids = service.data[ATTR_ENTITY_ID]
minutes = service.data[ATTR_TIME_PERIOD]
timeToEnd = int(time.mktime(datetime.timetuple(now()))+(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:
nest_water_heater.turn_boost_mode_on(timeToEnd)
else:
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(WaterHeaterEntity):
"""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
self._attr_supported_features = SUPPORTED_FEATURES
self._attr_operation_list = SUPPORTED_OPERATIONS
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
@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 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_attributes(self):
"""Return the optional state attributes."""
data = {}
supported_features = self.supported_features
# Operational mode will be off or schedule
if WaterHeaterEntityFeature.OPERATION_MODE in supported_features:
data[ATTR_OPERATION_MODE] = self.current_operation
# This is, is the away mode feature turned on/off
if WaterHeaterEntityFeature.AWAY_MODE in supported_features:
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 WaterHeaterEntityFeature.AWAY_MODE in supported_features:
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 WaterHeaterEntityFeature.ON_OFF in supported_features: # using ON_OFF as a proxy for 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_status']:
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
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 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()