Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Pyecobee

A Python implementation of the ecobee API.

Pyecobee is a simple, elegant, and object oriented implementation of the ecobee API in Python. Requests and responses are Python objects: Pyecobee serializes them to and from the JSON the ecobee API expects, so you never have to build or parse JSON yourself.

Requires Python 3.12 or newer.

[!IMPORTANT] ecobee no longer accepts new developer registrations. Pyecobee can therefore only be used by people who already have a valid ecobee application key. Version 2.0.0b1 has extensive offline test coverage but has not been tested against the live ecobee API. Existing API users willing to test it are encouraged to report their results.

[!WARNING] Earlier Pyecobee versions were tested with an ecobee Smart Si, but the version 2 modernization has not been tested against a live account. The following methods were also outside the historical live testing, though they should work. Please open an issue, or better still a pull request, if you hit a problem with any of them.

  • control_plug: requires an ecobee smart plug
  • reset_preferences: wipes a thermostat's settings
  • set_occupied: requires an EMS thermostat
  • unlink_voice_engine: requires a thermostat with voice control
  • update_sensor: requires an ecobee3 or ecobee4 thermostat
  • All hierarchy requests, accessible to EMS and Utility accounts only: list_hierarchy_sets, list_hierarchy_users, add_hierarchy_set, remove_hierarchy_set, rename_hierarchy_set, move_hierarchy_set, add_hierarchy_users, remove_hierarchy_users, unregister_hierarchy_users, update_hierarchy_users, register_hierarchy_thermostats, unregister_hierarchy_thermostats, move_hierarchy_thermostats, assign_hierarchy_thermostats
  • All utility requests, accessible to Utility accounts only: list_demand_responses, issue_demand_response, cancel_demand_response, issue_demand_managements
  • All runtime report job requests, accessible to Utility accounts only: create_runtime_report_job, list_runtime_report_job_status, cancel_runtime_report_job

[!NOTE] ecobee currently returns 404 for the object definitions of ECPDemandResponse and EquipmentUtilization. Responses stay forward compatible because unknown fields are ignored, and Runtime.equipment_utilization is kept as an untyped value until ecobee publishes its shape.

Any ecobee API keys and authorization, access or refresh tokens in the examples below are fake.

Installation

pip install pyecobee

Quick start

from pyecobee import EcobeeService, Selection, SelectionType, Tokens

ecobee_service = EcobeeService(
    "My Thermostat",
    "jiNXJ2Q6dyeAPXxy4HsFGUp1nK94C9VF",
    Tokens(),  # or the credentials you stored last time
    save_tokens,  # called whenever new credentials are issued
)

# One-time authorization: display the PIN, register the app at ecobee.com, then request tokens.
authorize_response = ecobee_service.authorize()
print(f"Enter this PIN at ecobee.com => {authorize_response.ecobee_pin}")
input("Press Enter once the app is authorized...")
ecobee_service.request_tokens()

thermostat_response = ecobee_service.request_thermostats(
    Selection(
        selection_type=SelectionType.REGISTERED,
        selection_match="",
        include_runtime=True,
    )
)
thermostat = thermostat_response.thermostat_list[0]
print(thermostat.name, thermostat.runtime.actual_temperature)

Pyecobee hands every new set of credentials to the callback and renews expired access tokens on its own. See Credentials.

Requests and responses

Building a request

Request models accept Python field names and serialize to ecobee's camelCase field names. Model construction rejects unknown fields, and enum fields require valid enum values.

from pyecobee import Selection, SelectionType

selection = Selection(
    selection_type=SelectionType.THERMOSTATS,
    selection_match="123456789012",
    include_runtime=True,
)

assert selection.to_api_dict() == {
    "selectionType": "thermostats",
    "selectionMatch": "123456789012",
    "includeRuntime": True,
}

Processing a response

Service methods deserialize responses into typed models. Access Python attributes, or use model_dump() when an alias-based JSON-compatible mapping is needed.

response = ecobee_service.request_thermostats(selection)
response.status.code  # 0 indicates success

thermostat = response.thermostat_list[0]
payload = response.model_dump(by_alias=True, exclude_none=True, mode="json")

Response deserialization ignores fields that ecobee adds after this release, including fields within nested objects.

Every model provides pretty_format() for alias-based diagnostic output, alongside the usual repr():

>>> repr(authorize_response)
"EcobeeAuthorizeResponse(ecobee_pin='bv29', code='...', scope='smartWrite', expires_in=9, interval=30)"

>>> authorize_response.pretty_format()
"EcobeeAuthorizeResponse({'code': '...', 'ecobeePin': 'bv29', ...})"

General usage

The EcobeeService class provides the ecobee API implementation. To use Pyecobee:

  1. Import the models you need.
  2. Instantiate an EcobeeService object.
  3. Complete the authorization sequence if required (authorize then request_tokens).
  4. Invoke the ecobee API requests and functions you need. Access tokens renew automatically.

Pyecobee ships with docstrings throughout. Use dir() and help() to explore:

>>> from pyecobee import EcobeeService
>>> dir(EcobeeService)
>>> help(EcobeeService)

Import the models

from pyecobee import EcobeeService, Selection, SelectionType, Thermostat, Tokens

# Import other models, enums, and exceptions by their explicit names.

Instantiate an EcobeeService object

ecobee_service = EcobeeService(
    "My Thermostat",  # a label of your choosing
    "jiNXJ2Q6dyeAPXxy4HsFGUp1nK94C9VF",  # your application key
    Tokens(),  # the credentials you hold, if any
    save_tokens,  # where to put the credentials ecobee issues
)

Every parameter is required. You pass the credentials you hold as tokens and get back every new set through on_tokens_changed, because the ecobee API requires an application to store what it is issued and replaces the refresh token every time it issues one.

Authorization and token requests

Authorize

authorize_response = ecobee_service.authorize()
print(f'Enter PIN "{authorize_response.ecobee_pin}" at ecobee.com')

A successful invocation of authorize() returns an EcobeeAuthorizeResponse instance.

Request tokens

token_response = ecobee_service.request_tokens()

A successful invocation of request_tokens() returns an EcobeeTokensResponse instance.

Refresh tokens

Renewal is automatic, so this is only needed to renew on your own schedule. See Token renewal.

token_response = ecobee_service.refresh_tokens()

A successful invocation of refresh_tokens() returns an EcobeeTokensResponse instance.

Thermostat requests

Request thermostat summary

thermostat_summary_response = ecobee_service.request_thermostats_summary(
    selection=Selection(
        selection_type=SelectionType.REGISTERED,
        selection_match="",
        include_equipment_status=True,
    )
)

A successful invocation of request_thermostats_summary() returns an EcobeeThermostatsSummaryResponse instance.

Request thermostats

# Only set the include options you need to True. Most are set to True here for illustrative purposes only.
selection = Selection(
    selection_type=SelectionType.REGISTERED,
    selection_match="",
    include_alerts=True,
    include_audio=True,
    include_capabilities=True,
    include_device=True,
    include_energy=True,
    include_equipment_status=True,
    include_events=True,
    include_extended_runtime=True,
    include_house_details=True,
    include_location=True,
    include_management=True,
    include_notification_settings=True,
    include_oem_cfg=False,
    include_privacy=False,
    include_program=True,
    include_reminders=True,
    include_runtime=True,
    include_security_settings=False,
    include_sensors=True,
    include_settings=True,
    include_technician=True,
    include_utility=True,
    include_version=True,
    include_weather=True,
)
thermostat_response = ecobee_service.request_thermostats(selection)
assert thermostat_response.status.code == 0, (
    f"Failure while executing request_thermostats:\n{thermostat_response.pretty_format()}"
)

A successful invocation of request_thermostats() returns an EcobeeThermostatResponse instance.

Update thermostat

from pyecobee import Function, Selection, SelectionType, Settings, Thermostat

update_thermostat_response = ecobee_service.update_thermostats(
    selection=Selection(selection_type=SelectionType.REGISTERED, selection_match=""),
    thermostat=Thermostat(
        identifier="123456789012", settings=Settings(hvac_mode="off")
    ),
    functions=[Function(type="deleteVacation", params={"name": "My vacation"})],
)
assert update_thermostat_response.status.code == 0, (
    f"Failure while executing update_thermostats:\n{update_thermostat_response.pretty_format()}"
)

A successful invocation of update_thermostats() returns an EcobeeStatusResponse instance.

Report requests

Meter report

from datetime import datetime
from zoneinfo import ZoneInfo

eastern = ZoneInfo("America/New_York")
meter_reports_response = ecobee_service.request_meter_reports(
    selection=Selection(
        selection_type=SelectionType.THERMOSTATS,
        selection_match="123456789012",
    ),
    start_date_time=datetime(2013, 4, 4, 0, 0, 0, tzinfo=eastern),
    end_date_time=datetime(2013, 4, 4, 23, 59, 0, tzinfo=eastern),
)
assert meter_reports_response.status.code == 0, (
    f"Failure while executing request_meter_reports:\n{meter_reports_response.pretty_format()}"
)

A successful invocation of request_meter_reports() returns an EcobeeMeterReportsResponse instance.

Runtime report

eastern = ZoneInfo("America/New_York")
runtime_reports_response = ecobee_service.request_runtime_reports(
    selection=Selection(
        selection_type=SelectionType.THERMOSTATS,
        selection_match="123456789012",
    ),
    start_date_time=datetime(2010, 1, 1, 0, 0, 0, tzinfo=eastern),
    end_date_time=datetime(2010, 1, 2, 0, 0, 0, tzinfo=eastern),
    columns=(
        "auxHeat1,auxHeat2,auxHeat3,compCool1,compCool2,compHeat1,compHeat2,dehumidifier,dmOffset,"
        "economizer,fan,humidifier,hvacMode,outdoorHumidity,outdoorTemp,sky,ventilator,wind,zoneAveTemp,"
        "zoneCalendarEvent,zoneClimate,zoneCoolTemp,zoneHeatTemp,zoneHumidity,zoneHumidityHigh,"
        "zoneHumidityLow,zoneHvacMode,zoneOccupancy"
    ),
)
assert runtime_reports_response.status.code == 0, (
    f"Failure while executing request_runtime_reports:\n{runtime_reports_response.pretty_format()}"
)

A successful invocation of request_runtime_reports() returns an EcobeeRuntimeReportsResponse instance.

Group requests

Request groups

group_response = ecobee_service.request_groups(
    selection=Selection(selection_type=SelectionType.REGISTERED, selection_match="")
)
assert group_response.status.code == 0, (
    f"Failure while executing request_groups:\n{group_response.pretty_format()}"
)

A successful invocation of request_groups() returns an EcobeeGroupsResponse instance.

Update groups

from pyecobee import Group

# Create groups
group_response = ecobee_service.update_groups(
    selection=Selection(selection_type=SelectionType.REGISTERED, selection_match=""),
    groups=[
        Group(
            group_ref="3d03a26fd80001",
            group_name="ground_floor",
            synchronize_alerts=True,
            synchronize_vacation=True,
            thermostats=["123456789101"],
        ),
        Group(
            group_ref="3bb5a91b180001",
            group_name="first_floor",
            synchronize_reset=True,
            synchronize_vacation=True,
            thermostats=["123456789102"],
        ),
    ],
)

# Update a group
group_response = ecobee_service.update_groups(
    selection=Selection(selection_type=SelectionType.REGISTERED, selection_match=""),
    groups=[
        Group(
            group_name="ground_floor",
            group_ref="3d03a26fd80001",
            synchronize_system_mode=True,
        )
    ],
)

# Delete a group by setting its thermostats to an empty list
group_response = ecobee_service.update_groups(
    selection=Selection(selection_type=SelectionType.REGISTERED, selection_match=""),
    groups=[
        Group(group_name="ground_floor", group_ref="3d03a26fd80001", thermostats=[])
    ],
)
assert group_response.status.code == 0, (
    f"Failure while executing update_groups:\n{group_response.pretty_format()}"
)

A successful invocation of update_groups() returns an EcobeeGroupsResponse instance.

Hierarchy set requests

List hierarchy sets

list_hierarchy_sets_response = ecobee_service.list_hierarchy_sets(
    set_path="/",
    recursive=True,
    include_privileges=True,
    include_thermostats=True,
)
assert list_hierarchy_sets_response.status.code == 0, (
    f"Failure while executing list_hierarchy_sets:\n{list_hierarchy_sets_response.pretty_format()}"
)

A successful invocation of list_hierarchy_sets() returns an EcobeeListHierarchySetsResponse instance.

Add hierarchy set

add_hierarchy_set_response = ecobee_service.add_hierarchy_set(
    set_name="NewSet", parent_path="/"
)

Remove hierarchy set

remove_hierarchy_set_response = ecobee_service.remove_hierarchy_set(set_path="/NewSet")

Rename hierarchy set

rename_hierarchy_set_response = ecobee_service.rename_hierarchy_set(
    set_path="/NewSet", new_name="ToRename"
)

Move hierarchy set

move_hierarchy_set_response = ecobee_service.move_hierarchy_set(
    set_path="/ToMove", to_path="MainNode"
)

A successful invocation of add_hierarchy_set(), remove_hierarchy_set(), rename_hierarchy_set() or move_hierarchy_set() returns an EcobeeStatusResponse instance.

Hierarchy user requests

List hierarchy users

list_hierarchy_users_response = ecobee_service.list_hierarchy_users(
    set_path="/",
    recursive=True,
    include_privileges=True,
)
assert list_hierarchy_users_response.status.code == 0, (
    f"Failure while executing list_hierarchy_users:\n{list_hierarchy_users_response.pretty_format()}"
)

A successful invocation of list_hierarchy_users() returns an EcobeeListHierarchyUsersResponse instance.

Add hierarchy users

from pyecobee import HierarchyPrivilege, HierarchyUser

add_hierarchy_users_response = ecobee_service.add_hierarchy_users(
    users=[
        HierarchyUser(user_name="new@user1.com", first_name="User", last_name="1"),
        HierarchyUser(user_name="new@user2.com", first_name="User", last_name="2"),
    ],
    privileges=[
        HierarchyPrivilege(
            set_path="/MainNode", user_name="new@user1.com", allow_view=True
        ),
        HierarchyPrivilege(
            set_path="/OtherNode", user_name="new@user1.com", allow_view=True
        ),
    ],
)
assert add_hierarchy_users_response.status.code == 0, (
    f"Failure while executing add_hierarchy_users:\n{add_hierarchy_users_response.pretty_format()}"
)

Remove hierarchy users

remove_hierarchy_users_response = ecobee_service.remove_hierarchy_users(
    set_path="/",
    users=[
        HierarchyUser(user_name="todelete@hierarchy.com"),
        HierarchyUser(user_name="todelete2@hierarchy.com"),
    ],
)

Unregister hierarchy users

unregister_hierarchy_users_response = ecobee_service.unregister_hierarchy_users(
    users=[
        HierarchyUser(user_name="todelete@hierarchy.com"),
        HierarchyUser(user_name="todelete2@hierarchy.com"),
    ]
)

Update hierarchy users

update_hierarchy_users_response = ecobee_service.update_hierarchy_users(
    users=[
        HierarchyUser(
            user_name="user1@update.com",
            first_name="Updated",
            last_name="User",
            phone="222-333-4444",
            email_alerts=False,
        )
    ],
    privileges=[
        HierarchyPrivilege(
            set_path="/MainNode", user_name="user1@update.com", allow_view=True
        ),
        HierarchyPrivilege(
            set_path="/MainNode", user_name="user2@update.com", allow_view=True
        ),
        HierarchyPrivilege(
            set_path="/OtherNode", user_name="user2@update.com", allow_view=True
        ),
    ],
)

A successful invocation of add_hierarchy_users(), remove_hierarchy_users(), unregister_hierarchy_users() or update_hierarchy_users() returns an EcobeeStatusResponse instance.

Hierarchy thermostat requests

Register thermostats

register_hierarchy_thermostats_response = ecobee_service.register_hierarchy_thermostats(
    set_path="/OtherNode",
    thermostats="123456789012,123456789013",
)

Unregister thermostats

unregister_hierarchy_thermostats_response = (
    ecobee_service.unregister_hierarchy_thermostats(
        thermostats="123456789012,123456789013"
    )
)

Move thermostats

move_hierarchy_thermostats_response = ecobee_service.move_hierarchy_thermostats(
    set_path="/MainNode",
    to_path="/OtherNode",
    thermostats="123456789012,123456789013",
)

Assign thermostats

assign_hierarchy_thermostats_response = ecobee_service.assign_hierarchy_thermostats(
    set_path="/MainNode",
    thermostats="123456789012,123456789013",
)

A successful invocation of any hierarchy thermostat request returns an EcobeeStatusResponse instance.

Utility requests

List demand responses

list_demand_responses_response = ecobee_service.list_demand_responses()
assert list_demand_responses_response.status.code == 0, (
    f"Failure while executing list_demand_responses:\n{list_demand_responses_response.pretty_format()}"
)

A successful invocation of list_demand_responses() returns an EcobeeListDemandResponsesResponse instance.

Issue demand response

from pyecobee import DemandResponse, Event

issue_demand_response_response = ecobee_service.issue_demand_response(
    selection=Selection(
        selection_type=SelectionType.MANAGEMENT_SET, selection_match="/"
    ),
    demand_response=DemandResponse(
        name="myDR",
        message="This is a DR!",
        event=Event(
            type="useEndTime",
            name="apiDR",
            start_date="2011-01-09",
            start_time="11:37:18",
            end_date="2011-01-10",
            end_time="11:37:18",
            cool_hold_temp=790,
            heat_hold_temp=790,
            is_temperature_absolute=True,
        ),
    ),
)
assert issue_demand_response_response.status.code == 0, (
    f"Failure while executing issue_demand_response:\n{issue_demand_response_response.pretty_format()}"
)

A successful invocation of issue_demand_response() returns an EcobeeIssueDemandResponsesResponse instance.

Cancel demand response

cancel_demand_response_response = ecobee_service.cancel_demand_response(
    demand_response_ref="c253a12e0b3c3c93800095"
)

A successful invocation of cancel_demand_response() returns an EcobeeStatusResponse instance.

Issue demand managements

from pyecobee import DemandManagement

issue_demand_managements_response = ecobee_service.issue_demand_managements(
    selection=Selection(
        selection_type=SelectionType.MANAGEMENT_SET, selection_match="/"
    ),
    demand_managements=[
        DemandManagement(
            date="2012-01-01",
            hour=5,
            temp_offsets=[20, 20, 20, 0, 0, 0, 0, -20, -20, -20, 0, 0],
        ),
        DemandManagement(
            date="2012-01-01",
            hour=6,
            temp_offsets=[0, 0, 20, 20, 0, 0, 0, 0, 0, -20, -20, -20],
        ),
    ],
)

A successful invocation of issue_demand_managements() returns an EcobeeStatusResponse instance.

Runtime report job requests

Create runtime report job

from datetime import date

create_runtime_report_job_response = ecobee_service.create_runtime_report_job(
    selection=Selection(
        selection_type=SelectionType.THERMOSTATS, selection_match="123456789012"
    ),
    start_date=date(2016, 7, 1),
    end_date=date(2016, 10, 1),
    columns="zoneCalendarEvent,zoneHvacMode,zoneHeatTemp,zoneCoolTemp,zoneAveTemp,dmOffset",
)
assert create_runtime_report_job_response.status.code == 0, (
    f"Failure while executing create_runtime_report_job:\n{create_runtime_report_job_response.pretty_format()}"
)

A successful invocation of create_runtime_report_job() returns an EcobeeCreateRuntimeReportJobResponse instance.

List runtime report job status

list_runtime_report_job_status_response = ecobee_service.list_runtime_report_job_status(
    job_id="123"
)

A successful invocation of list_runtime_report_job_status() returns an EcobeeListRuntimeReportJobStatusResponse instance.

Cancel runtime report job

cancel_runtime_report_job_response = ecobee_service.cancel_runtime_report_job(
    job_id="123"
)

A successful invocation of cancel_runtime_report_job() returns an EcobeeStatusResponse instance.

Thermostat functions

A successful invocation of any thermostat function returns an EcobeeStatusResponse instance.

Send message

update_thermostat_response = ecobee_service.send_message("Hello World")
assert update_thermostat_response.status.code == 0, (
    f"Failure while executing send_message:\n{update_thermostat_response.pretty_format()}"
)

Acknowledge

from pyecobee import AckType

selection = Selection(
    selection_type=SelectionType.REGISTERED,
    selection_match="",
    include_alerts=True,
)
thermostat_response = ecobee_service.request_thermostats(selection)
thermostat = thermostat_response.thermostat_list[0]
alerts = [alert for alert in thermostat.alerts if alert.text == message]

update_thermostat_response = ecobee_service.acknowledge(
    thermostat_identifier=thermostat.identifier,
    ack_ref=alerts[0].acknowledge_ref,
    ack_type=AckType.ACCEPT,
)

Set hold

from pyecobee import HoldType

# Simplest form
update_thermostat_response = ecobee_service.set_hold(
    hold_climate_ref="away",
    hold_type=HoldType.NEXT_TRANSITION,
)

# Using a specific start and end date and time
eastern = ZoneInfo("America/New_York")
update_thermostat_response = ecobee_service.set_hold(
    hold_climate_ref="away",
    start_date_time=datetime(2017, 5, 10, 13, 0, 0, tzinfo=eastern),
    end_date_time=datetime(2017, 5, 10, 14, 0, 0, tzinfo=eastern),
    hold_type=HoldType.DATE_TIME,
)

# Using a duration
update_thermostat_response = ecobee_service.set_hold(
    hold_climate_ref="away",
    start_date_time=datetime(2017, 5, 10, 13, 0, 0, tzinfo=eastern),
    hold_type=HoldType.HOLD_HOURS,
    hold_hours=1,
)

# Specific heating and cooling temperatures, held indefinitely
update_thermostat_response = ecobee_service.set_hold(
    cool_hold_temp=75,
    heat_hold_temp=68,
    hold_type=HoldType.INDEFINITE,
)

Resume program

update_thermostat_response = ecobee_service.resume_program(resume_all=False)

Create vacation

from pyecobee import FanMode

eastern = ZoneInfo("America/New_York")
update_thermostat_response = ecobee_service.create_vacation(
    name="Christmas Vacation!",
    cool_hold_temp=104,
    heat_hold_temp=59,
    start_date_time=datetime(2017, 12, 23, 10, 0, 0, tzinfo=eastern),
    end_date_time=datetime(2017, 12, 28, 17, 0, 0, tzinfo=eastern),
    fan_mode=FanMode.AUTO,
    fan_min_on_time=0,
)

Delete vacation

update_thermostat_response = ecobee_service.delete_vacation(name="Christmas Vacation!")

Reset preferences

# Danger zone! This resets every user configurable setting to its factory default.
update_thermostat_response = ecobee_service.reset_preferences()

Credentials

The ecobee API requires that every credential it issues is stored by the application, and it replaces the refresh token each time it issues one. Pyecobee therefore takes the credentials you hold and a callback to store the ones it receives.

JsonFileTokenStore writes the credentials as JSON in a file only your user can read, which is all most applications need. Its load and save are the two arguments the service asks for.

from pyecobee import EcobeeService, JsonFileTokenStore

store = JsonFileTokenStore("~/.config/pyecobee/tokens.json")
ecobee_service = EcobeeService(
    "My Thermostat",
    application_key,
    store.load(),
    store.save,
)

The file is written under a temporary name and renamed into place, so an interrupted save cannot leave a half-written file where your credentials used to be.

If the storage callback raises, Pyecobee does not suppress the exception. The newly issued credentials remain available through ecobee_service.tokens while the process is still running.

[!WARNING] Catch storage callback exceptions and recover ecobee_service.tokens before the process exits. By the time the callback runs, ecobee has already invalidated the previous refresh token. If the process exits before the new credentials are stored, they are lost and the application must authorize again.

Anywhere else you want to keep them, supply your own pair. Tokens is an immutable snapshot that converts to and from a plain mapping through to_dict and from_dict, so a store is usually a few lines. For a desktop application, consider keyring. For a server, take the application key and the initial credentials from the environment or a secret manager.

Tokens omits the credentials from its representation, so logging one discloses only the expiries and the scope:

>>> ecobee_service.tokens
Tokens(held=access_token, refresh_token, access_token_expires_on=..., refresh_token_expires_on=..., scope=<Scope.SMART_WRITE: 'smartWrite'>)

First run

Authorization is only needed when no credentials are held. The callback stores whatever each step produces.

if ecobee_service.authorization_token is None:
    authorize_response = ecobee_service.authorize()
    input(
        "Go to ecobee.com, enable My Apps in the settings tab, and install "
        f'PIN "{authorize_response.ecobee_pin}". Press Enter here after ecobee '
        "authorizes the app."
    )

if ecobee_service.access_token is None:
    ecobee_service.request_tokens()

Token renewal

An access token expires 3599 seconds (1 hour) after it is issued. A refresh token expires 30 days after it is issued, and the ecobee API does not report that expiry, so Pyecobee derives it.

Renewal is automatic. Before each request Pyecobee renews an access token that is within two minutes of expiring, and if ecobee answers that the token has already expired, it renews the credentials and sends the request once more. Each renewal reaches your callback.

# No token handling required. The request renews the credentials if it has to.
thermostat_summary_response = ecobee_service.request_thermostats_summary(
    selection=Selection(
        selection_type=SelectionType.REGISTERED,
        selection_match="",
        include_equipment_status=True,
    )
)

Renewal needs a refresh token that is still valid. Once one has expired, ecobee raises EcobeeAuthorizationException and the application must authorize again:

from pyecobee import EcobeeAuthorizationException

try:
    thermostat_response = ecobee_service.request_thermostats(selection)
except EcobeeAuthorizationException:
    authorize_again(ecobee_service)

refresh_tokens() remains available for an application that would rather renew on its own schedule, such as before a long idle period.

Date and time handling

Some ecobee API requests expect a date and time in thermostat time, while others expect UTC.

Every EcobeeService method that accepts a datetime expects it in thermostat time, and the datetime must be timezone aware.

from datetime import datetime
from zoneinfo import ZoneInfo

eastern = ZoneInfo("America/New_York")
start_date_time = datetime(
    2017, 5, 1, 10, 0, 0, tzinfo=eastern
)  # 2017/05/01 10:00:00 -0400

The method then either uses the datetime as is, or converts it to UTC, depending on what the ecobee API request requires.

Exception handling

Your code should be prepared to handle the following exceptions:

  • EcobeeApiException: raised if a request results in an ecobee API error response. Exposes status_code and status_message.
  • EcobeeAuthorizationException: raised if a request results in a standard or extended OAuth error response. Exposes error, error_description and error_uri.
  • EcobeeRequestsException: raised if a request results in an exception from the underlying requests module.
  • EcobeeHttpException: raised if a request results in any other HTTP error.
  • EcobeeDeserializationException: raised if a response cannot be converted into a model.

All of them derive from EcobeeException.

Development

Pyecobee is developed with uv and Ruff. To create the locked development environment:

uv sync --locked

Run the lint and formatting checks:

uv run ruff check .
uv run ruff format --check .

Run the offline regression suite, which enforces an 82% coverage minimum:

uv run pytest

tests/live_integration.py is not collected by pytest. It requires an existing valid ecobee application key, contacts a real account, exercises only requests that read data, and stores its credentials in ~/.config/pyecobee/live_integration.json:

ECOBEE_APPLICATION_KEY=your_application_key uv run python tests/live_integration.py

Apply safe lint fixes, sort imports, and format the source tree:

uv run ruff check . --select I --fix
uv run ruff check . --fix
uv run ruff format .

See CHANGELOG.md for release notes and MIGRATION.md for the 1.x to 2.0 upgrade path.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyecobee-2.0.0b1.tar.gz (79.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyecobee-2.0.0b1-py3-none-any.whl (53.0 kB view details)

Uploaded Python 3

File details

Details for the file pyecobee-2.0.0b1.tar.gz.

File metadata

  • Download URL: pyecobee-2.0.0b1.tar.gz
  • Upload date:
  • Size: 79.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pyecobee-2.0.0b1.tar.gz
Algorithm Hash digest
SHA256 e83ffb3597f0faea5a599c39573f25b6935bd0fe97efa8229546a837b3460db3
MD5 718f0228bd4d02f5ae11a0017dea1368
BLAKE2b-256 e6cbfe9534c7cd4fec99921f40879fa281e46dbb54f5f6b33f9dcab9dadbeff4

See more details on using hashes here.

File details

Details for the file pyecobee-2.0.0b1-py3-none-any.whl.

File metadata

  • Download URL: pyecobee-2.0.0b1-py3-none-any.whl
  • Upload date:
  • Size: 53.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pyecobee-2.0.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 ab745e0c99a828c18234bd2748eec5a6cdb510955e64c56d7e942a43badb187e
MD5 02e21f8fa9c3c19aefc18e2c6bd6e590
BLAKE2b-256 2c61cb5b25bef09b5c4838cc565f6d3577eb796678ac5a9a8a1600d286b56a4e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page