First published 12/3/2023

As always, opinions in this post are solely those of my own, and not necessarily those of any organization I am currently affiliated with or have been in the past.

While this might seem like a departure from the network engineering theme of my other homelab content, I’ve been having a blast automating smart HVAC gear APIs recently and was surprised at the lack of content available on this topic. Brace yourselves for at least a few posts regarding HVAC coming soon! Sure, there’s tons of good stuff at HomeAssistant, but what if I want a dirt simple Python script to do the following?

  1. Using the ecobee API, turn my basement thermostat controlling the boiler up to “home” comfort setting of 21.5 degrees (70 degrees in freedom units)
  2. Using the dyson fan API, turn my basement fan on so my toddler’s playroom gets enough airflow to warm up
  3. Have one easy-to-remember alias in my Windows Subsystem for Linux setup so I can easily turn the ecobee/dyson combo on or off without opening a web browser or my smartphone

Now I know the 3 goals above are super niche as most people use something like HomeKit or similar integrations. But I really wanted to have my own simple Python script so I can easily run it when I’m working from home and my toddler wakes up from afternoon naptime. Otherwise once I’m done with work we’re stuck playing in a very cold concrete room, or I open up the Ecobee/Dyson apps on my smartphone during the workday and get distracted by all the other trash on-screen. So without further adieu, let’s start playing with APIs to warm up my kiddo’s play area, affectionately known as the “slide room” since this all started with a hand-me-down red slide, growing into today’s wonderland:

Step 1: Get ecobee API access

Thanks to HomeAssistant’s write-up, it’s pretty easy to get API access into ecobee. Start by following their “Preliminary Steps” instructions verbatim to get an API key with ecobee PIN authorization method. Once you have the API key in-hand, hit up https://www.ecobee.com/home/developer/api/examples/ex1.shtml to use their example Javascript ecobeePin generator:

Save the code somewhere special (that’s the authorization code), then go back to your ecobee.com portal account, click on the “My Apps” menu (this is next to the fancy new Developer menu you unlocked in the HomeAssistant write-up), and enter the PIN to get your code activated. You’ll get a cool little message like this, then you’re good to go!

Once you’ve gotten past that authentication piece, scroll down to step 2 of https://www.ecobee.com/home/developer/api/examples/ex1.shtml and enter the API key + auth code to generate an access token + refresh token. Perform step 3 once you have both; make sure you have the API key and refresh token saved!

Step 2: Basic Python to handle the API key + refresh token

Note: see my GitHub folder for this homelab if you’re planning to try this at home

Now that we have good credentials, I asked my good buddy ChatGPT4 to cook up some Python that will generate that access token for us, and GPT4 came through!

If everything went well, you should be able to run the test1.py script and receive a clean JSON reply containing an access token. If this is your first time playing with Python, don’t worry, check out my very first Python homelab as it will help you get the ball rolling, consider the Hello World book too.

With a little extra help from ChatGPT, I got the following going in test2.py to return info about all three of my thermostats:

The output of this command will give you a bunch of information about your ecobee thermostats, but all we really care about is the 12-digit identifier. This is pretty easy to find as the “name” field is right next to the identifier in our JSON output, so once you have it, fire up in test3.py to do a basic test of grabbing thermostat temperature, then finally use test4.py to turn on the heat & set comfort setting to “home”:

Success! We’ll come back to the ecobee API in a bit since we want to include code for turning off the heat too. It’s hard to describe how happy I was to hear the boiler running in the basement after doing this for the first time, running this Python script is sooooo much more enjoyable than opening up the ecobee app or website to set the temp & turn on the heat!

Step 3: Get Dyson API access

Long story short, Dyson enabled MFA on their cloud accounts in 2021, and this broke a ton of stuff with libpurecool, which was the most popular way of messing with Dyson fans via Python. Fortunately, a new project has refactored all of that code at https://github.com/libdyson-wg/libdyson-neon, and with just a little extra work we can turn our Dyson fan on & off via Python completely via the LAN! Only problem is this means you’ll need to have direct LAN access to the fan, so if you’ve firewalled off your Internet-of-Things stuff from the rest of your home network (which we’re all totally doing, right?!?), make sure to get the firewall rules squared away before continuing. You’ll also need to get the IP address of your Dyson fan and have it registered in the regular smartphone app too.

Go ahead and do a “pip3 install libdyson-neon” from your terminal, and in some random temporary directory do a “https://github.com/libdyson-wg/libdyson-neon.git”. Once you’ve cloned that repo, run the get_devices.py script to generate the credential needed for accessing the fan via local area network (note the verification code is from the Dyson 2FA email, kudos to the maintainers of libdyson-neon for making this so seamless, it took hours of me googling/ChatGPTing to find their fix!):

Now that we have our coveted credential, go ahead and run the test5.py script to turn the fan on, which verifies our API access is working well. If you have issues, I highly recommend checking out https://www.williamperdue.com/post/dyson-graph-part-1/, his walkthrough helped me quite a bit.

Step 4: Tie it all together with a big script to turn everything on, and another to turn it all off

Now that we have all our building blocks together in those 5 test Python scrips, let’s tie it all together with slideroom_on.py to turn on the ecobee heat & turn on the Dyson fan via a one-liner I can easily write an alias for in my bashrc, saving tons of time when I want to start warming up the kiddo’s basement play palace:

import requests
import json
import libdyson

#Act 1: Get Ecobee Access Token

def get_ecobee_access_token(api_key, refresh_token):
    url = "https://api.ecobee.com/token"
    payload = {
        'grant_type': 'refresh_token',
        'refresh_token': refresh_token,
        'client_id': api_key
    }
    response = requests.post(url, params=payload)
    if response.status_code == 200:
        return response.json()  # This will return the access token and a new refresh token
    else:
        return response.json()  # This will contain error information

# Replace 'your_api_key' and 'your_refresh_token' with your actual ecobee API key and refresh token
api_key = 'your_api_key'
refresh_token = 'your_refresh_token'

access_token_info = get_ecobee_access_token(api_key, refresh_token)
access_token = access_token_info['access_token']

# Act 2: Get current temp just for fun, then set ecobee to heat at home comfort setting

def get_thermostat_temperature(access_token, thermostat_id):
    url = "https://api.ecobee.com/1/thermostat"
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': 'application/json'
    }
    params = {
        'json': '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostat_id + '","includeRuntime":true}}'
    }
    response = requests.get(url, headers=headers, params=params)
    if response.status_code == 200:
        data = response.json()
        # Extracting temperature from the response
        thermostat_info = data['thermostatList'][0]
        current_temperature = thermostat_info['runtime']['actualTemperature'] / 10  # The temperature is reported in tenths of degrees
        return current_temperature
    else:
        return response.json()  # This will contain error information

# Replace 'your_thermostat_id' with your actual thermostat ID
thermostat_id = 'your_thermostat_id'  # The identifier of your thermostat

temperature = get_thermostat_temperature(access_token, thermostat_id)
print(f"Current temperature: {temperature}°F")

def set_hvac_mode_to_heat(access_token, thermostat_id):
    url = "https://api.ecobee.com/1/thermostat?format=json"
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': 'application/json'
    }

    # Create the JSON payload to update the HVAC mode
    payload = {
        'selection': {
            'selectionType': 'thermostats',
            'selectionMatch': thermostat_id
        },
        'thermostat': {
            'settings': {
                'hvacMode': 'heat'
            }
        }
    }

    response = requests.post(url, headers=headers, json=payload)
    if response.status_code == 200:
        return "HVAC mode set to heat successfully."
    else:
        return response.json()  # This will contain error information

result = set_hvac_mode_to_heat(access_token, thermostat_id)
print(result)


def set_comfort_setting_to_home(access_token, thermostat_id):
    url = "https://api.ecobee.com/1/thermostat?format=json"
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': 'application/json'
    }

    # Create the JSON payload to set the comfort setting to 'Home'
    payload = {
        'selection': {
            'selectionType': 'thermostats',
            'selectionMatch': thermostat_id
        },
        'functions': [{
            'type': 'resumeProgram',
            'params': {
                'resumeAll': False
            }
        }]
    }

    response = requests.post(url, headers=headers, json=payload)
    if response.status_code == 200:
        return "Comfort setting set to 'Home' successfully."
    else:
        return response.json()  # This will contain error information

result = set_comfort_setting_to_home(access_token, thermostat_id)
print(result)

# Act 3: Turn on the Dyson fan

# Replace these with your actual device's credentials
DEVICE_SERIAL = "your-dyson-serial"
DEVICE_CREDENTIAL = "your-dyson-credential"
DEVICE_IP = "your-dyson-ip"

# Connect to the device
device = libdyson.DysonPureHotCoolLink(serial=DEVICE_SERIAL, credential=DEVICE_CREDENTIAL, device_type=libdyson.DEVICE_TYPE_PURE_HOT_COOL_LINK)
device.connect(DEVICE_IP)

# Turn on the fan
device.turn_on()
device.disconnect()
print('Dyson fan appears to have turned on if no errors present above')

Then once we’re done playing, the slideroom_off.py script can be ran to shut it all down:

import requests
import json
import libdyson

#Act 1: Get Ecobee Access Token

def get_ecobee_access_token(api_key, refresh_token):
    url = "https://api.ecobee.com/token"
    payload = {
        'grant_type': 'refresh_token',
        'refresh_token': refresh_token,
        'client_id': api_key
    }
    response = requests.post(url, params=payload)
    if response.status_code == 200:
        return response.json()  # This will return the access token and a new refresh token
    else:
        return response.json()  # This will contain error information

# Replace 'your_api_key' and 'your_refresh_token' with your actual ecobee API key and refresh token
api_key = 'your_api_key'
refresh_token = 'your_refresh_token'

access_token_info = get_ecobee_access_token(api_key, refresh_token)
access_token = access_token_info['access_token']

# Act 2: Turn ecobee off

def turn_off_heat(access_token, thermostat_id):
    url = "https://api.ecobee.com/1/thermostat?format=json"
    headers = {
        'Authorization': f'Bearer {access_token}',
        'Content-Type': 'application/json'
    }

    # Create the JSON payload to turn off the HVAC (heating)
    payload = {
        'selection': {
            'selectionType': 'thermostats',
            'selectionMatch': thermostat_id
        },
        'thermostat': {
            'settings': {
                'hvacMode': 'off'
            }
        }
    }

    response = requests.post(url, headers=headers, json=payload)
    if response.status_code == 200:
        return "Heating turned off successfully."
    else:
        return response.json()  # This will contain error information

# Replace 'your_thermostat_id' with your actual thermostat ID
thermostat_id = 'your_thermostat_id'  # The identifier of your thermostat

result = turn_off_heat(access_token, thermostat_id)
print(result)

# Act 3: Turn off the Dyson fan

# Replace these with your actual device's credentials
DEVICE_SERIAL = "your-dyson-serial"
DEVICE_CREDENTIAL = "your-dyson-credential"
DEVICE_IP = "your-dyson-ip"

# Connect to the device
device = libdyson.DysonPureHotCoolLink(serial=DEVICE_SERIAL, credential=DEVICE_CREDENTIAL, device_type=libdyson.DEVICE_TYPE_PURE_HOT_COOL_LINK)
device.connect(DEVICE_IP)

# Turn on the fan
device.turn_off()
device.disconnect()
print('Dyson fan appears to have turned off if no errors present above')

Closing Thoughts

Although some may call it cheating, I am very much proud of the fact I used ChatGPT to do most of the heavy Python lifting for this project. Sure, I should probably be better at writing basic API stuff with requests by now, but the truth is ChatGPT has been producing way cleaner code than what I’d write myself on this matter. Now that being said, none of this code would have worked if I didn’t know the basics of Python, formatting, indentation, etc., so it’s not like ChatGPT did everything for us. While I have no doubt I could have figured out everything done in this homelab on my own with enough trial & error, I think it’s fair to say AI sped up the development process in an immeasurable way. Whether ChatGPT saved me 5 hours or 50 hours is unclear, but I’ll take any help I can get in the Python department!

There’s still a ton of things I could be doing better with these scripts. Setting the Dyson fan speed would be cool, a cleaner experience to get the ecobee tokens setup without their example website would be fantastic, and I think this will all break in a year due to the expiration date of ecobee refresh tokens. If you get around to making any of that better, let me know here or send a pull request on GitHub, would love to feature your work here too!

You’ve reached the end of the post! Click here to go back to the list of all Homelab posts.

You should also know I use Amazon Affiliate links to defray the cost of otherwise ad-free webhosting

HVAC Homelab Part 1: Python fun with Ecobee & Dyson APIs

Post navigation


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.