DIY Smart Thermostat <--> GHome via HASS?

Hi All,

I hope I’m posting in the right place. I have spent a few days reading documentation, experimenting with my own HASS install, etc, but still haven’t quite figured out how to accomplish my goal. I apologize for the wall of text, but any insight would be greatly appreciated.

More than a year ago I built a smart thermostat for my home. It is essentially a raspberry pi with a tornado/jsonrpc-over-websocket interface. It has its own touch lcd on the wall, but I’ve found we mostly use it through the web page on our cell phones.

Back in November I got my first Google Home Mini, and decided I’d like to somehow get voice support via the google home to control my thermostat. I don’t want to use “Actions on Google” because that’s just dumb. What I do want is to be able to trigger assistant on my phone, or google home and say things like “raise the temperature”, or “set the temperature to 72”, etc.

I did get a program running via the Google Assistant SDK, but that would mean I can’t use the Google Homes, or phone assistants.

So here is where HASS comes in… maybe. I’ve installed HASS, set up encryption on my domain, and followed the tutorial for adding Google Assistant to HASS. All good so far. From this point, it looks like I should be able to write a custom module within

<HASS PATH>/lib/.../site-packages/homeassistant/components/climate/

which would have the necessary support for talking to my thermostat, and then instantiate that component from within HASS.

Am I on the right track with this? Am I missing something that would completely break my whole plan? Or is there a better way to accomplish this?

Thank you again for any help you might be able to offer!

Edit: Pictures of my thermostat, because… projects:

Smart Thermostat Album - Google Photos

3 Likes

Hello and welcome to our community!

You can create a folder inside Hass called custom_components where you can write, test and modified components. But you are on the right track!

I just bought a new smart thermostat from Honeywell yesterday and looking at your project I already regret it :smiley:

If you could make the Thermostat communicate with MQTT integration with Hass would be alot easier

Hi Sthope, thank you for your reply and advice.

I’ve looked into adding MQTT to my thermostat code, that shouldn’t be too hard to do, so I’m definitely considering that route.

Assuming I get HASS support to my thermostat, and having already followed the Google Assistant setup tutorial, would I then be able to control my thermostat through google assistant via HASS? I feel like there is still a murky cloud of something happening inside HASS that I’m just not seeing.

Does the Google Assistant component setup procedure allow HASS to essentially “intercept” Google Assistant commands?

Thanks again for your help!

Hi, pdenson.

I’m still using emulated_hue and haven’t tested much of the new component yet, so my devices are all exposed to Alexa and Google Home as Philips lights.
https://home-assistant.io/components/google_assistant/ exposes components normally.

If you add MQTT to your thermostat you might need to create a MQTT thermostat component or maybe modify the https://home-assistant.io/components/climate.generic_thermostat/ in Hass so everything is controllable normally though Google Home.

Google Assistant component allows you to voice control every component entity in Home Assistant.

I just added https://home-assistant.io/components/climate.generic_thermostat/ to my config linked a relay and a dht11 to it and name it ‘baby heating’ and installed Google Assistant component to test it.

I was able to control the temperature of the thermostat by saying: “Hey Google, set the baby heating to 25 degrees”

Just what I was hoping to hear. Thanks again Sthope!

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

I’m getting the same response from GA with my custom climate device.

Looking in the logs I see this:

File "/usr/lib/python3.6/site-packages/homeassistant/components/google_assistant/smart_home.py", line 240, in query_response_climate
    mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
AttributeError: 'NoneType' object has no attribute 'lower'

Looking at the script in google_assistant/smart_home.py here is the code:

@QUERY_HANDLERS.register(climate.DOMAIN)
def query_response_climate(
        entity: Entity, config: Config, units: UnitSystem) -> dict:
    """Convert a climate entity to a QUERY response."""
    mode = entity.attributes.get(climate.ATTR_OPERATION_MODE)
    if mode is None:
        mode = entity.state
    mode = mode.lower()
    if mode not in CLIMATE_SUPPORTED_MODES:
        mode = 'heat'
    attrs = entity.attributes
    response = {
        'thermostatMode': mode,
        'thermostatTemperatureSetpoint':
        celsius(attrs.get(climate.ATTR_TEMPERATURE), units),
        'thermostatTemperatureAmbient':
        celsius(attrs.get(climate.ATTR_CURRENT_TEMPERATURE), units),
        'thermostatTemperatureSetpointHigh':
        celsius(attrs.get(climate.ATTR_TARGET_TEMP_HIGH), units),
        'thermostatTemperatureSetpointLow':
        celsius(attrs.get(climate.ATTR_TARGET_TEMP_LOW), units),
        'thermostatHumidityAmbient':
        attrs.get(climate.ATTR_CURRENT_HUMIDITY),
    }
    return {k: v for k, v in response.items() if v is not None}

not sure why it’s failing on the lower() function

That is odd.

I finally got mine working a few days ago. It turned out that re-syncing my Google Developer Console project was failing silently, and I assumed it was succeeding. I didn’t notice the failure until I tried to make changes elsewhere.

Eventually, I had to delete my Console project and recreate it from scratch. Once that was done and it was re-linked to hass everything worked great.

Could you upload the relevant code from your custom component/platform? I.e. the ‘set_operation_mode’ or ‘async_set_operation_mode’ method?

not seeing that function in my script, which is most likely the issue. Just need to figure out how to add it properly since only heat is supported.

actually my issue was fixed in 0.64.0 https://github.com/home-assistant/home-assistant/pull/12279

Very nice! Any chance that you’ll be sharing your code and/or graphics? I have a working MQTT thermostat put together for my in-floor electric radiant heat based on an ESP8266 but haven’t gotten around to putting together an interface for it yet. Too many projects! :smiley:

actually my issue was fixed in 0.64.0

Glad to hear its working for you now!

Very nice! Any chance that you’ll be sharing your code and/or graphics?

Thanks!

Here is a link to my repo on gitlab. I’m pretty happy with the code as it is. The one thing I need to fix is that I have to build a .so from a c++ driver I wrote for my temp sensor. But getting python3 support in boost-python requires building the boost libraries from scratch… and that just sucks.

Either way, if all you want is the graphics, then you probably wouldn’t care about that anyway.

@pdenson I have been looking for something like this for a very long while now. Even though I set up most commands using iftt so I can control my rpi thermostat just fine. I was looking for a way to query my Google home for the current temp but with no luck. Anyways, I was trying to take a look at your repo but I am getting a 404 error. I am not sure if you changed the privacy settings on it but I would really appreciate it if you can reshare it with me.