I just wanted to report in that I got this 95% working!
I have written a custom platform which implements a ClimateDevice. I can instantiate and control my home built thermostat from within HASS, setting the mode of operation (heat, cool, off, etc.) and target temperatures. It works more-or-less perfectly.
I’ve also implemented the Google Assistant component. Using GA from my phone or Google Home, I can query my thermostat for the current temperature, or set a target temperature. However, if I ask GA to set the thermostat mode it just replies… “That mode isn’t available for the upstairs thermostat.”
I’ve tried looking at other thermostat implementations but I still don’t see what I might be missing…
Any ideas?
TIA!
import json
import asyncio
import asyncws
from jsonrpcclient import Request
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.components.climate import (
ClimateDevice,
STATE_HEAT, STATE_COOL, STATE_FAN_ONLY, STATE_IDLE,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_HOLD_MODE,
PLATFORM_SCHEMA, ATTR_OPERATION_MODE)
from homeassistant.const import (
CONF_URL, CONF_NAME,
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE |
SUPPORT_HOLD_MODE)
# Validation of the user's configuration
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL, default='localhost:8000/ws'): cv.string,
vol.Optional(CONF_NAME, default='thermostat'): cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Setup the Awesome Light platform."""
url = config.get(CONF_URL)
name = config.get(CONF_NAME)
# Add devices
async_add_entities([DCC_Thermostat(hass, url, name)])
class DCC_Thermostat(ClimateDevice):
"""Simplified interface to a denson.cc thermostat."""
def __init__(self, hass, url, name):
"""Initialize a DCC_Thermostat."""
self._url = url
self._name = name
self._status = None # Store whole json status
self._mode = None # Set mode
self._state = None # Current operating mode (state)
self._units = hass.config.units.temperature_unit
self._target_temp = 69.0
self._cur_temp = 69.0
self._cur_humidity = 50.0
_LOGGER.info("DCC_Thermostat initialized (%s - %s)",
self._name, self._url)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def name(self):
"""Return the display name of this thermostat."""
return self._name
@property
def state(self):
"""Return the current state."""
if not self._state:
return STATE_OFF
elif self._state == 'heat':
return STATE_HEAT
elif self._state == 'cool':
return STATE_COOL
elif self._state == 'fan':
return STATE_FAN_ONLY
else:
return STATE_IDLE
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
if not self._mode:
return STATE_OFF
elif self._mode == 'heat':
return STATE_HEAT
elif self._mode == 'cool':
return STATE_COOL
elif self._mode == 'fan':
return STATE_FAN_ONLY
else:
return STATE_OFF
@property
def is_on(self):
"""Return true if on."""
return self._state != 'off'
@property
def current_fan_mode(self):
"""Return the fan setting."""
if self._state != 'off':
return STATE_ON
return STATE_OFF
@property
def operation_list(self):
"""Return the list of available operation modes."""
return [STATE_HEAT, STATE_COOL, STATE_FAN_ONLY, STATE_OFF]
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._units
@property
def current_temperature(self):
"""Return the sensor temperature."""
return int(round(self._cur_temp))
@property
def target_temperature(self):
"""Return the target temperature."""
return int(round(self._target_temp))
@property
def current_humidity(self):
"""Return the sensor temperature."""
return int(round(self._cur_humidity))
@asyncio.coroutine
def async_update(self):
_LOGGER.info('Connecting to %s...', self._url)
ws = yield from asyncws.connect(self._url)
_LOGGER.info('Connected')
rqst = Request('get_status')
_LOGGER.info('Sending status request...')
yield from ws.send(json.dumps(rqst))
_LOGGER.info('Awaiting response...')
status = yield from ws.recv()
_LOGGER.info('Response received.')
ws.close()
_LOGGER.info('Status: %s', str(status))
self._status = json.loads(status)['result']
self._mode = self._status['hvac']['mode']
self._state = self._status['hvac']['state']
# Have to convert Celcius to Farenheit
self._target_temp = self.CtoF(self._status['hvac']['target'])
self._cur_temp = float(self._status['hih8120']['tempF'])
self._humidity = float(self._status['hih8120']['humidity'])
@asyncio.coroutine
def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
self._target_temp = temperature
ws = yield from asyncws.connect(self._url)
# Have to convert Farenheit to Celcius
rqst = Request('set_target', target_temp=self.FtoC(self._target_temp))
yield from ws.send(json.dumps(rqst))
result = yield from ws.recv()
ws.close()
_LOGGER.info('Set Temp Result: %s', str(result))
@asyncio.coroutine
def async_set_operation_mode(self, operation_mode):
"""Set operation mode."""
_LOGGER.warning("Setting Async Mode! %s", str(operation_mode))
if operation_mode == STATE_OFF:
self._mode = 'off'
elif operation_mode == STATE_HEAT:
self._mode = 'heat'
elif operation_mode == STATE_COOL:
self._mode = 'cool'
elif operation_mode == STATE_FAN_ONLY:
self._mode = 'fan'
else:
self._mode = 'off'
ws = yield from asyncws.connect(self._url)
rqst = Request('set_mode', mode=self._mode)
yield from ws.send(json.dumps(rqst))
result = yield from ws.recv()
ws.close()
_LOGGER.info('Set Mode Result: %s', str(result))
def CtoF(self, C):
return (float(C)*1.8)+32.0
def FtoC(self, F):
return (float(F)-32.0)/1.8