├── hacs.json ├── custom_components └── nexia │ ├── .idea │ ├── encodings.xml │ ├── modules.xml │ ├── misc.xml │ ├── nexia.iml │ └── workspace.xml │ ├── util.py │ ├── manifest.json │ ├── strings.json │ ├── .translations │ ├── en.json │ ├── no.json │ ├── it.json │ ├── ca.json │ ├── lb.json │ ├── es.json │ ├── de.json │ ├── fr.json │ ├── zh-Hant.json │ ├── ko.json │ └── ru.json │ ├── services.yaml │ ├── const.py │ ├── binary_sensor.py │ ├── scene.py │ ├── config_flow.py │ ├── __init__.py │ ├── entity.py │ ├── sensor.py │ └── climate.py ├── info.md ├── README.md └── LICENSE /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nexia Climate Integration", 3 | "render_readme": true, 4 | "domains": ["sensor", "binary_sensor", "climate"], 5 | "iot_class": "Local Polling" 6 | } 7 | -------------------------------------------------------------------------------- /custom_components/nexia/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /custom_components/nexia/util.py: -------------------------------------------------------------------------------- 1 | """Utils for Nexia / Trane XL Thermostats.""" 2 | 3 | 4 | def percent_conv(val): 5 | """Convert an actual percentage (0.0-1.0) to 0-100 scale.""" 6 | return round(val * 100.0, 1) 7 | -------------------------------------------------------------------------------- /custom_components/nexia/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "nexia", 3 | "name": "Nexia", 4 | "requirements": ["nexia==0.8.2"], 5 | "codeowners": ["@ryannazaretian", "@bdraco"], 6 | "documentation": "https://www.home-assistant.io/integrations/nexia", 7 | "config_flow": true 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/nexia/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /custom_components/nexia/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | The `nexia` component lets you control thermostats connected to [Nexia (Trane/American Standard)](https://www.nexiahome.com/). 2 | 3 | By connecting this component, you will have access to all thermostats and zones in your associated home. 4 | 5 | See the [Github readme](https://github.com/ryannazaretian/hacs-nexia-climate-integration) for installation and configuration. 6 | 7 | [Buy me a coffee?](https://www.buymeacoffee.com/ybLHaPf) -------------------------------------------------------------------------------- /custom_components/nexia/.idea/nexia.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /custom_components/nexia/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "Nexia", 4 | "step": { 5 | "user": { 6 | "title": "Connect to mynexia.com", 7 | "data": { 8 | "username": "Username", 9 | "password": "Password" 10 | } 11 | } 12 | }, 13 | "error": { 14 | "cannot_connect": "Failed to connect, please try again", 15 | "invalid_auth": "Invalid authentication", 16 | "unknown": "Unexpected error" 17 | }, 18 | "abort": { 19 | "already_configured": "This nexia home is already configured" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /custom_components/nexia/.translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "This nexia home is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect, please try again", 8 | "invalid_auth": "Invalid authentication", 9 | "unknown": "Unexpected error" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "Password", 15 | "username": "Username" 16 | }, 17 | "title": "Connect to mynexia.com" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/.translations/no.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Dette nexia hjem er allerede konfigurert" 5 | }, 6 | "error": { 7 | "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", 8 | "invalid_auth": "Ugyldig godkjenning", 9 | "unknown": "Uventet feil" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "Passord", 15 | "username": "Brukernavn" 16 | }, 17 | "title": "Koble til mynexia.com" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/.translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Questo Nexia Home \u00e8 gi\u00e0 configurato" 5 | }, 6 | "error": { 7 | "cannot_connect": "Impossibile connettersi, si prega di riprovare", 8 | "invalid_auth": "Autenticazione non valida", 9 | "unknown": "Errore imprevisto" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "Password", 15 | "username": "Nome utente" 16 | }, 17 | "title": "Connettersi a mynexia.com" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/.translations/ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Aquest dispositiu nexia home ja est\u00e0 configurat" 5 | }, 6 | "error": { 7 | "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", 8 | "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", 9 | "unknown": "Error inesperat" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "Contrasenya", 15 | "username": "Nom d'usuari" 16 | }, 17 | "title": "Connexi\u00f3 amb mynexia.com" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/.translations/lb.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Den Nexia Home ass scho konfigur\u00e9iert" 5 | }, 6 | "error": { 7 | "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", 8 | "invalid_auth": "Ong\u00eblteg Authentifikatioun", 9 | "unknown": "Onerwaarte Feeler" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "Passwuert", 15 | "username": "Benotzernumm" 16 | }, 17 | "title": "Mat mynexia.com verbannen" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/.translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Este nexia home ya est\u00e1 configurado" 5 | }, 6 | "error": { 7 | "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", 8 | "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", 9 | "unknown": "Error inesperado" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "Contrase\u00f1a", 15 | "username": "Usuario" 16 | }, 17 | "title": "Conectar con mynexia.com" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/.translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Dieses Nexia Home ist bereits konfiguriert" 5 | }, 6 | "error": { 7 | "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", 8 | "invalid_auth": "Ung\u00fcltige Authentifizierung", 9 | "unknown": "Unerwarteter Fehler" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "Passwort", 15 | "username": "Benutzername" 16 | }, 17 | "title": "Stellen Sie eine Verbindung zu mynexia.com her" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/.translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Cette maison Nexia est d\u00e9j\u00e0 configur\u00e9e" 5 | }, 6 | "error": { 7 | "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", 8 | "invalid_auth": "Authentification non valide", 9 | "unknown": "Erreur inattendue" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "Mot de passe", 15 | "username": "Nom d'utilisateur" 16 | }, 17 | "title": "Se connecter \u00e0 mynexia.com" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/services.yaml: -------------------------------------------------------------------------------- 1 | set_aircleaner_mode: 2 | description: "The air cleaner mode." 3 | fields: 4 | entity_id: 5 | description: "This setting will affect all zones connected to the thermostat." 6 | example: climate.master_bedroom 7 | aircleaner_mode: 8 | description: 'The air cleaner mode to set. Options include "auto", "quick", or "allergy".' 9 | example: allergy 10 | 11 | set_humidify_setpoint: 12 | description: "The humidification set point." 13 | fields: 14 | entity_id: 15 | description: "This setting will affect all zones connected to the thermostat." 16 | example: climate.master_bedroom 17 | humidity: 18 | description: "The humidification setpoint as an int, range 35-65." 19 | example: 45 20 | -------------------------------------------------------------------------------- /custom_components/nexia/.translations/zh-Hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Nexia home \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" 5 | }, 6 | "error": { 7 | "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", 8 | "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", 9 | "unknown": "\u672a\u9810\u671f\u932f\u8aa4" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "\u5bc6\u78bc", 15 | "username": "\u4f7f\u7528\u8005\u540d\u7a31" 16 | }, 17 | "title": "\u9023\u7dda\u81f3 mynexia.com" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/const.py: -------------------------------------------------------------------------------- 1 | """Nexia constants.""" 2 | 3 | PLATFORMS = ["sensor", "binary_sensor", "climate", "scene"] 4 | 5 | ATTRIBUTION = "Data provided by mynexia.com" 6 | 7 | NOTIFICATION_ID = "nexia_notification" 8 | NOTIFICATION_TITLE = "Nexia Setup" 9 | 10 | NEXIA_DEVICE = "device" 11 | NEXIA_SCAN_INTERVAL = "scan_interval" 12 | 13 | DOMAIN = "nexia" 14 | DEFAULT_ENTITY_NAMESPACE = "nexia" 15 | 16 | ATTR_DESCRIPTION = "description" 17 | 18 | ATTR_AIRCLEANER_MODE = "aircleaner_mode" 19 | 20 | ATTR_ZONE_STATUS = "zone_status" 21 | ATTR_HUMIDIFY_SUPPORTED = "humidify_supported" 22 | ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported" 23 | ATTR_HUMIDIFY_SETPOINT = "humidify_setpoint" 24 | ATTR_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint" 25 | 26 | UPDATE_COORDINATOR = "update_coordinator" 27 | 28 | MANUFACTURER = "Trane" 29 | 30 | SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE" 31 | SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE" 32 | -------------------------------------------------------------------------------- /custom_components/nexia/.translations/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "nexia home \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" 5 | }, 6 | "error": { 7 | "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", 8 | "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", 9 | "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "\ube44\ubc00\ubc88\ud638", 15 | "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" 16 | }, 17 | "title": "mynexia.com \uc5d0 \uc5f0\uacb0\ud558\uae30" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/.translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." 5 | }, 6 | "error": { 7 | "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", 8 | "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", 9 | "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "\u041f\u0430\u0440\u043e\u043b\u044c", 15 | "username": "\u041b\u043e\u0433\u0438\u043d" 16 | }, 17 | "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a mynexia.com" 18 | } 19 | }, 20 | "title": "Nexia" 21 | } 22 | } -------------------------------------------------------------------------------- /custom_components/nexia/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Nexia / Trane XL Thermostats.""" 2 | 3 | from homeassistant.components.binary_sensor import BinarySensorDevice 4 | 5 | from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR 6 | from .entity import NexiaThermostatEntity 7 | 8 | 9 | async def async_setup_entry(hass, config_entry, async_add_entities): 10 | """Set up sensors for a Nexia device.""" 11 | 12 | nexia_data = hass.data[DOMAIN][config_entry.entry_id] 13 | nexia_home = nexia_data[NEXIA_DEVICE] 14 | coordinator = nexia_data[UPDATE_COORDINATOR] 15 | 16 | entities = [] 17 | for thermostat_id in nexia_home.get_thermostat_ids(): 18 | thermostat = nexia_home.get_thermostat_by_id(thermostat_id) 19 | entities.append( 20 | NexiaBinarySensor( 21 | coordinator, thermostat, "is_blower_active", "Blower Active" 22 | ) 23 | ) 24 | if thermostat.has_emergency_heat(): 25 | entities.append( 26 | NexiaBinarySensor( 27 | coordinator, 28 | thermostat, 29 | "is_emergency_heat_active", 30 | "Emergency Heat Active", 31 | ) 32 | ) 33 | 34 | async_add_entities(entities, True) 35 | 36 | 37 | class NexiaBinarySensor(NexiaThermostatEntity, BinarySensorDevice): 38 | """Provices Nexia BinarySensor support.""" 39 | 40 | def __init__(self, coordinator, thermostat, sensor_call, sensor_name): 41 | """Initialize the nexia sensor.""" 42 | super().__init__( 43 | coordinator, 44 | thermostat, 45 | name=f"{thermostat.get_name()} {sensor_name}", 46 | unique_id=f"{thermostat.thermostat_id}_{sensor_call}", 47 | ) 48 | self._call = sensor_call 49 | self._state = None 50 | 51 | @property 52 | def is_on(self): 53 | """Return the status of the sensor.""" 54 | return getattr(self._thermostat, self._call)() 55 | -------------------------------------------------------------------------------- /custom_components/nexia/scene.py: -------------------------------------------------------------------------------- 1 | """Support for Nexia Automations.""" 2 | 3 | from homeassistant.components.scene import Scene 4 | from homeassistant.helpers.event import async_call_later 5 | 6 | from .const import ATTR_DESCRIPTION, DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR 7 | from .entity import NexiaEntity 8 | 9 | SCENE_ACTIVATION_TIME = 5 10 | 11 | 12 | async def async_setup_entry(hass, config_entry, async_add_entities): 13 | """Set up automations for a Nexia device.""" 14 | 15 | nexia_data = hass.data[DOMAIN][config_entry.entry_id] 16 | nexia_home = nexia_data[NEXIA_DEVICE] 17 | coordinator = nexia_data[UPDATE_COORDINATOR] 18 | entities = [] 19 | 20 | # Automation switches 21 | for automation_id in nexia_home.get_automation_ids(): 22 | automation = nexia_home.get_automation_by_id(automation_id) 23 | 24 | entities.append(NexiaAutomationScene(coordinator, automation)) 25 | 26 | async_add_entities(entities, True) 27 | 28 | 29 | class NexiaAutomationScene(NexiaEntity, Scene): 30 | """Provides Nexia automation support.""" 31 | 32 | def __init__(self, coordinator, automation): 33 | """Initialize the automation scene.""" 34 | super().__init__( 35 | coordinator, name=automation.name, unique_id=automation.automation_id, 36 | ) 37 | self._automation = automation 38 | 39 | @property 40 | def device_state_attributes(self): 41 | """Return the scene specific state attributes.""" 42 | data = super().device_state_attributes 43 | data[ATTR_DESCRIPTION] = self._automation.description 44 | return data 45 | 46 | @property 47 | def icon(self): 48 | """Return the icon of the automation scene.""" 49 | return "mdi:script-text-outline" 50 | 51 | async def async_activate(self): 52 | """Activate an automation scene.""" 53 | await self.hass.async_add_executor_job(self._automation.activate) 54 | 55 | async def refresh_callback(_): 56 | await self._coordinator.async_refresh() 57 | 58 | async_call_later(self.hass, SCENE_ACTIVATION_TIME, refresh_callback) 59 | -------------------------------------------------------------------------------- /custom_components/nexia/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Nexia integration.""" 2 | import logging 3 | 4 | from nexia.home import NexiaHome 5 | from requests.exceptions import ConnectTimeout, HTTPError 6 | import voluptuous as vol 7 | 8 | from homeassistant import config_entries, core, exceptions 9 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 10 | 11 | from .const import DOMAIN # pylint:disable=unused-import 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str}) 16 | 17 | 18 | async def validate_input(hass: core.HomeAssistant, data): 19 | """Validate the user input allows us to connect. 20 | 21 | Data has the keys from DATA_SCHEMA with values provided by the user. 22 | """ 23 | try: 24 | nexia_home = NexiaHome( 25 | username=data[CONF_USERNAME], 26 | password=data[CONF_PASSWORD], 27 | auto_login=False, 28 | auto_update=False, 29 | device_name=hass.config.location_name, 30 | ) 31 | await hass.async_add_executor_job(nexia_home.login) 32 | except ConnectTimeout as ex: 33 | _LOGGER.error("Unable to connect to Nexia service: %s", ex) 34 | raise CannotConnect 35 | except HTTPError as http_ex: 36 | _LOGGER.error("HTTP error from Nexia service: %s", http_ex) 37 | if http_ex.response.status_code >= 400 and http_ex.response.status_code < 500: 38 | raise InvalidAuth 39 | raise CannotConnect 40 | 41 | if not nexia_home.get_name(): 42 | raise InvalidAuth 43 | 44 | info = {"title": nexia_home.get_name(), "house_id": nexia_home.house_id} 45 | _LOGGER.debug("Setup ok with info: %s", info) 46 | return info 47 | 48 | 49 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 50 | """Handle a config flow for Nexia.""" 51 | 52 | VERSION = 1 53 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 54 | 55 | async def async_step_user(self, user_input=None): 56 | """Handle the initial step.""" 57 | errors = {} 58 | if user_input is not None: 59 | try: 60 | info = await validate_input(self.hass, user_input) 61 | except CannotConnect: 62 | errors["base"] = "cannot_connect" 63 | except InvalidAuth: 64 | errors["base"] = "invalid_auth" 65 | except Exception: # pylint: disable=broad-except 66 | _LOGGER.exception("Unexpected exception") 67 | errors["base"] = "unknown" 68 | 69 | if "base" not in errors: 70 | await self.async_set_unique_id(info["house_id"]) 71 | self._abort_if_unique_id_configured() 72 | return self.async_create_entry(title=info["title"], data=user_input) 73 | 74 | return self.async_show_form( 75 | step_id="user", data_schema=DATA_SCHEMA, errors=errors 76 | ) 77 | 78 | async def async_step_import(self, user_input): 79 | """Handle import.""" 80 | return await self.async_step_user(user_input) 81 | 82 | 83 | class CannotConnect(exceptions.HomeAssistantError): 84 | """Error to indicate we cannot connect.""" 85 | 86 | 87 | class InvalidAuth(exceptions.HomeAssistantError): 88 | """Error to indicate there is invalid auth.""" 89 | -------------------------------------------------------------------------------- /custom_components/nexia/__init__.py: -------------------------------------------------------------------------------- 1 | """Support for Nexia / Trane XL Thermostats.""" 2 | import asyncio 3 | from datetime import timedelta 4 | from functools import partial 5 | import logging 6 | 7 | from nexia.home import NexiaHome 8 | from requests.exceptions import ConnectTimeout, HTTPError 9 | import voluptuous as vol 10 | 11 | from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry 12 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 13 | from homeassistant.core import HomeAssistant 14 | from homeassistant.exceptions import ConfigEntryNotReady 15 | import homeassistant.helpers.config_validation as cv 16 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 17 | 18 | from .const import DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | 23 | CONFIG_SCHEMA = vol.Schema( 24 | { 25 | DOMAIN: vol.Schema( 26 | { 27 | vol.Required(CONF_USERNAME): cv.string, 28 | vol.Required(CONF_PASSWORD): cv.string, 29 | }, 30 | extra=vol.ALLOW_EXTRA, 31 | ), 32 | }, 33 | extra=vol.ALLOW_EXTRA, 34 | ) 35 | 36 | DEFAULT_UPDATE_RATE = 120 37 | 38 | 39 | async def async_setup(hass: HomeAssistant, config: dict) -> bool: 40 | """Set up the nexia component from YAML.""" 41 | 42 | conf = config.get(DOMAIN) 43 | hass.data.setdefault(DOMAIN, {}) 44 | 45 | if not conf: 46 | return True 47 | 48 | hass.async_create_task( 49 | hass.config_entries.flow.async_init( 50 | DOMAIN, context={"source": SOURCE_IMPORT}, data=conf 51 | ) 52 | ) 53 | return True 54 | 55 | 56 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 57 | """Configure the base Nexia device for Home Assistant.""" 58 | 59 | conf = entry.data 60 | username = conf[CONF_USERNAME] 61 | password = conf[CONF_PASSWORD] 62 | 63 | try: 64 | nexia_home = await hass.async_add_executor_job( 65 | partial( 66 | NexiaHome, 67 | username=username, 68 | password=password, 69 | device_name=hass.config.location_name, 70 | ) 71 | ) 72 | except ConnectTimeout as ex: 73 | _LOGGER.error("Unable to connect to Nexia service: %s", ex) 74 | raise ConfigEntryNotReady 75 | except HTTPError as http_ex: 76 | if http_ex.response.status_code >= 400 and http_ex.response.status_code < 500: 77 | _LOGGER.error( 78 | "Access error from Nexia service, please check credentials: %s", 79 | http_ex, 80 | ) 81 | return False 82 | _LOGGER.error("HTTP error from Nexia service: %s", http_ex) 83 | raise ConfigEntryNotReady 84 | 85 | async def _async_update_data(): 86 | """Fetch data from API endpoint.""" 87 | return await hass.async_add_job(nexia_home.update) 88 | 89 | coordinator = DataUpdateCoordinator( 90 | hass, 91 | _LOGGER, 92 | name="Nexia update", 93 | update_method=_async_update_data, 94 | update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), 95 | ) 96 | 97 | hass.data[DOMAIN][entry.entry_id] = { 98 | NEXIA_DEVICE: nexia_home, 99 | UPDATE_COORDINATOR: coordinator, 100 | } 101 | 102 | for component in PLATFORMS: 103 | hass.async_create_task( 104 | hass.config_entries.async_forward_entry_setup(entry, component) 105 | ) 106 | 107 | return True 108 | 109 | 110 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): 111 | """Unload a config entry.""" 112 | unload_ok = all( 113 | await asyncio.gather( 114 | *[ 115 | hass.config_entries.async_forward_entry_unload(entry, component) 116 | for component in PLATFORMS 117 | ] 118 | ) 119 | ) 120 | if unload_ok: 121 | hass.data[DOMAIN].pop(entry.entry_id) 122 | 123 | return unload_ok 124 | -------------------------------------------------------------------------------- /custom_components/nexia/entity.py: -------------------------------------------------------------------------------- 1 | """The nexia integration base entity.""" 2 | 3 | from homeassistant.const import ATTR_ATTRIBUTION 4 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 5 | from homeassistant.helpers.entity import Entity 6 | 7 | from .const import ( 8 | ATTRIBUTION, 9 | DOMAIN, 10 | MANUFACTURER, 11 | SIGNAL_THERMOSTAT_UPDATE, 12 | SIGNAL_ZONE_UPDATE, 13 | ) 14 | 15 | 16 | class NexiaEntity(Entity): 17 | """Base class for nexia entities.""" 18 | 19 | def __init__(self, coordinator, name, unique_id): 20 | """Initialize the entity.""" 21 | super().__init__() 22 | self._unique_id = unique_id 23 | self._name = name 24 | self._coordinator = coordinator 25 | 26 | @property 27 | def available(self): 28 | """Return True if entity is available.""" 29 | return self._coordinator.last_update_success 30 | 31 | @property 32 | def unique_id(self): 33 | """Return the unique id.""" 34 | return self._unique_id 35 | 36 | @property 37 | def name(self): 38 | """Return the name.""" 39 | return self._name 40 | 41 | @property 42 | def device_state_attributes(self): 43 | """Return the device specific state attributes.""" 44 | return { 45 | ATTR_ATTRIBUTION: ATTRIBUTION, 46 | } 47 | 48 | @property 49 | def should_poll(self): 50 | """Return False, updates are controlled via coordinator.""" 51 | return False 52 | 53 | async def async_added_to_hass(self): 54 | """Subscribe to updates.""" 55 | self._coordinator.async_add_listener(self.async_write_ha_state) 56 | 57 | async def async_will_remove_from_hass(self): 58 | """Undo subscription.""" 59 | self._coordinator.async_remove_listener(self.async_write_ha_state) 60 | 61 | 62 | class NexiaThermostatEntity(NexiaEntity): 63 | """Base class for nexia devices attached to a thermostat.""" 64 | 65 | def __init__(self, coordinator, thermostat, name, unique_id): 66 | """Initialize the entity.""" 67 | super().__init__(coordinator, name, unique_id) 68 | self._thermostat = thermostat 69 | self._thermostat_update_subscription = None 70 | 71 | @property 72 | def device_info(self): 73 | """Return the device_info of the device.""" 74 | return { 75 | "identifiers": {(DOMAIN, self._thermostat.thermostat_id)}, 76 | "name": self._thermostat.get_name(), 77 | "model": self._thermostat.get_model(), 78 | "sw_version": self._thermostat.get_firmware(), 79 | "manufacturer": MANUFACTURER, 80 | } 81 | 82 | async def async_added_to_hass(self): 83 | """Listen for signals for services.""" 84 | await super().async_added_to_hass() 85 | self._thermostat_update_subscription = async_dispatcher_connect( 86 | self.hass, 87 | f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}", 88 | self.async_write_ha_state, 89 | ) 90 | 91 | async def async_will_remove_from_hass(self): 92 | """Unsub from signals for services.""" 93 | await super().async_will_remove_from_hass() 94 | if self._thermostat_update_subscription: 95 | self._thermostat_update_subscription() 96 | 97 | 98 | class NexiaThermostatZoneEntity(NexiaThermostatEntity): 99 | """Base class for nexia devices attached to a thermostat.""" 100 | 101 | def __init__(self, coordinator, zone, name, unique_id): 102 | """Initialize the entity.""" 103 | super().__init__(coordinator, zone.thermostat, name, unique_id) 104 | self._zone = zone 105 | self._zone_update_subscription = None 106 | 107 | @property 108 | def device_info(self): 109 | """Return the device_info of the device.""" 110 | data = super().device_info 111 | data.update( 112 | { 113 | "identifiers": {(DOMAIN, self._zone.zone_id)}, 114 | "name": self._zone.get_name(), 115 | "via_device": (DOMAIN, self._zone.thermostat.thermostat_id), 116 | } 117 | ) 118 | return data 119 | 120 | async def async_added_to_hass(self): 121 | """Listen for signals for services.""" 122 | await super().async_added_to_hass() 123 | self._zone_update_subscription = async_dispatcher_connect( 124 | self.hass, 125 | f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}", 126 | self.async_write_ha_state, 127 | ) 128 | 129 | async def async_will_remove_from_hass(self): 130 | """Unsub from signals for services.""" 131 | await super().async_will_remove_from_hass() 132 | if self._zone_update_subscription: 133 | self._zone_update_subscription() 134 | -------------------------------------------------------------------------------- /custom_components/nexia/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Nexia / Trane XL Thermostats.""" 2 | 3 | from nexia.const import UNIT_CELSIUS 4 | 5 | from homeassistant.const import ( 6 | DEVICE_CLASS_HUMIDITY, 7 | DEVICE_CLASS_TEMPERATURE, 8 | TEMP_CELSIUS, 9 | TEMP_FAHRENHEIT, 10 | ) 11 | 12 | from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR 13 | from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity 14 | from .util import percent_conv 15 | 16 | 17 | async def async_setup_entry(hass, config_entry, async_add_entities): 18 | """Set up sensors for a Nexia device.""" 19 | 20 | nexia_data = hass.data[DOMAIN][config_entry.entry_id] 21 | nexia_home = nexia_data[NEXIA_DEVICE] 22 | coordinator = nexia_data[UPDATE_COORDINATOR] 23 | entities = [] 24 | 25 | # Thermostat / System Sensors 26 | for thermostat_id in nexia_home.get_thermostat_ids(): 27 | thermostat = nexia_home.get_thermostat_by_id(thermostat_id) 28 | 29 | entities.append( 30 | NexiaThermostatSensor( 31 | coordinator, 32 | thermostat, 33 | "get_system_status", 34 | "System Status", 35 | None, 36 | None, 37 | ) 38 | ) 39 | # Air cleaner 40 | entities.append( 41 | NexiaThermostatSensor( 42 | coordinator, 43 | thermostat, 44 | "get_air_cleaner_mode", 45 | "Air Cleaner Mode", 46 | None, 47 | None, 48 | ) 49 | ) 50 | # Compressor Speed 51 | if thermostat.has_variable_speed_compressor(): 52 | entities.append( 53 | NexiaThermostatSensor( 54 | coordinator, 55 | thermostat, 56 | "get_current_compressor_speed", 57 | "Current Compressor Speed", 58 | None, 59 | "%", 60 | percent_conv, 61 | ) 62 | ) 63 | entities.append( 64 | NexiaThermostatSensor( 65 | coordinator, 66 | thermostat, 67 | "get_requested_compressor_speed", 68 | "Requested Compressor Speed", 69 | None, 70 | "%", 71 | percent_conv, 72 | ) 73 | ) 74 | # Outdoor Temperature 75 | if thermostat.has_outdoor_temperature(): 76 | unit = ( 77 | TEMP_CELSIUS 78 | if thermostat.get_unit() == UNIT_CELSIUS 79 | else TEMP_FAHRENHEIT 80 | ) 81 | entities.append( 82 | NexiaThermostatSensor( 83 | coordinator, 84 | thermostat, 85 | "get_outdoor_temperature", 86 | "Outdoor Temperature", 87 | DEVICE_CLASS_TEMPERATURE, 88 | unit, 89 | ) 90 | ) 91 | # Relative Humidity 92 | if thermostat.has_relative_humidity(): 93 | entities.append( 94 | NexiaThermostatSensor( 95 | coordinator, 96 | thermostat, 97 | "get_relative_humidity", 98 | "Relative Humidity", 99 | DEVICE_CLASS_HUMIDITY, 100 | "%", 101 | percent_conv, 102 | ) 103 | ) 104 | 105 | # Zone Sensors 106 | for zone_id in thermostat.get_zone_ids(): 107 | zone = thermostat.get_zone_by_id(zone_id) 108 | unit = ( 109 | TEMP_CELSIUS 110 | if thermostat.get_unit() == UNIT_CELSIUS 111 | else TEMP_FAHRENHEIT 112 | ) 113 | # Temperature 114 | entities.append( 115 | NexiaThermostatZoneSensor( 116 | coordinator, 117 | zone, 118 | "get_temperature", 119 | "Temperature", 120 | DEVICE_CLASS_TEMPERATURE, 121 | unit, 122 | None, 123 | ) 124 | ) 125 | # Zone Status 126 | entities.append( 127 | NexiaThermostatZoneSensor( 128 | coordinator, zone, "get_status", "Zone Status", None, None, 129 | ) 130 | ) 131 | # Setpoint Status 132 | entities.append( 133 | NexiaThermostatZoneSensor( 134 | coordinator, 135 | zone, 136 | "get_setpoint_status", 137 | "Zone Setpoint Status", 138 | None, 139 | None, 140 | ) 141 | ) 142 | 143 | async_add_entities(entities, True) 144 | 145 | 146 | class NexiaThermostatSensor(NexiaThermostatEntity): 147 | """Provides Nexia thermostat sensor support.""" 148 | 149 | def __init__( 150 | self, 151 | coordinator, 152 | thermostat, 153 | sensor_call, 154 | sensor_name, 155 | sensor_class, 156 | sensor_unit, 157 | modifier=None, 158 | ): 159 | """Initialize the sensor.""" 160 | super().__init__( 161 | coordinator, 162 | thermostat, 163 | name=f"{thermostat.get_name()} {sensor_name}", 164 | unique_id=f"{thermostat.thermostat_id}_{sensor_call}", 165 | ) 166 | self._call = sensor_call 167 | self._class = sensor_class 168 | self._state = None 169 | self._unit_of_measurement = sensor_unit 170 | self._modifier = modifier 171 | 172 | @property 173 | def device_class(self): 174 | """Return the device class of the sensor.""" 175 | return self._class 176 | 177 | @property 178 | def state(self): 179 | """Return the state of the sensor.""" 180 | val = getattr(self._thermostat, self._call)() 181 | if self._modifier: 182 | val = self._modifier(val) 183 | if isinstance(val, float): 184 | val = round(val, 1) 185 | return val 186 | 187 | @property 188 | def unit_of_measurement(self): 189 | """Return the unit of measurement this sensor expresses itself in.""" 190 | return self._unit_of_measurement 191 | 192 | 193 | class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity): 194 | """Nexia Zone Sensor Support.""" 195 | 196 | def __init__( 197 | self, 198 | coordinator, 199 | zone, 200 | sensor_call, 201 | sensor_name, 202 | sensor_class, 203 | sensor_unit, 204 | modifier=None, 205 | ): 206 | """Create a zone sensor.""" 207 | 208 | super().__init__( 209 | coordinator, 210 | zone, 211 | name=f"{zone.get_name()} {sensor_name}", 212 | unique_id=f"{zone.zone_id}_{sensor_call}", 213 | ) 214 | self._call = sensor_call 215 | self._class = sensor_class 216 | self._state = None 217 | self._unit_of_measurement = sensor_unit 218 | self._modifier = modifier 219 | 220 | @property 221 | def device_class(self): 222 | """Return the device class of the sensor.""" 223 | return self._class 224 | 225 | @property 226 | def state(self): 227 | """Return the state of the sensor.""" 228 | val = getattr(self._zone, self._call)() 229 | if self._modifier: 230 | val = self._modifier(val) 231 | if isinstance(val, float): 232 | val = round(val, 1) 233 | return val 234 | 235 | @property 236 | def unit_of_measurement(self): 237 | """Return the unit of measurement this sensor expresses itself in.""" 238 | return self._unit_of_measurement 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The `nexia` component lets you control thermostats connected to [Nexia (Trane/American Standard)](https://www.nexiahome.com/). 2 | 3 | By connecting this component, you will have access to all thermostats and zones in your associated home. 4 | 5 | Like this? Love this? [Buy me a coffee?](https://www.buymeacoffee.com/ybLHaPf) 6 | 7 | ## Configuration 8 | 9 | To add the Nexia component to Home Assistant, add the following information to your [`configuration.yaml`](/docs/configuration/) file: 10 | 11 | You can get ```YOUR_HOUSE_ID``` by logging into [Nexia](https://www.nexiahome.com/), going to the climate section, 12 | and then getting the number that comes between ```https://www.mynexia.com/houses/``` and ```/climate```. 13 | 14 | ``` 15 | https://www.mynexia.com/houses//climate 16 | ``` 17 | 18 | You will put ```YOUR_HOUSE_ID``` into the configuration as an integer and not a string. 19 | 20 | ```yaml 21 | # Example configuration.yaml entry 22 | nexia: 23 | username: "YOUR_USERNAME" 24 | password: "YOUR_PASSWORD" 25 | id: YOUR_HOUSE_ID 26 | scan_interval: (optional) SECONDS_BETWEEN_UPDATES 27 | 28 | climate: 29 | - platform: nexia 30 | 31 | binary_sensor: 32 | - platform: nexia 33 | 34 | sensor: 35 | - platform: nexia 36 | 37 | ``` 38 | 39 | [Restart Home Assistant](https://www.home-assistant.io/docs/configuration/#reloading-changes) for the changes to take effect. 40 | 41 | ### Concepts 42 | 43 | The Nexia Thermostat supports the following key concepts. 44 | 45 | 46 | 47 | ## Attributes 48 | 49 | The following attributes are provided by the Nexia Thermostat 50 | `aux_heat`, `away_mode`, `current_humidity`, `current_temperature`, 51 | `fan_list`, `fan_mode`, `firmware`, `friendly_name`, `hold_mode`, `humidity`, `humidify_supported`, 52 | `dehumidify_supported`, `humidify_setpoint`, `dehumidify_setpoint` 53 | `max_humidity`, `max_temp`, `min_humidity`, `min_temp`, `model`, `operation_list`, 54 | `operation_mode`, `setpoint_status`, `target_temp_high`, `target_temp_low`, 55 | `target_temp_step`, `temperature`, `thermostat_id`, `thermostat_name`, `zone_id`, 56 | `zone_status` 57 | 58 | ### `aux_heat` 59 | 60 | Indicates whether or not aux heat / emergency heat is enabled. 61 | 62 | | Attribute type | Description | 63 | | -------------- | ----------- | 64 | | String | 'on' or 'off' | 65 | 66 | ### `away_mode` 67 | 68 | Indicates whether the 'away' preset is selected. 69 | 70 | | Attribute type | Description | 71 | | -------------- | ----------- | 72 | | String | 'on' or 'off' | 73 | 74 | ### Attribute `current_humidity` 75 | 76 | Provides you with the main thermostat's relative humidity. Outdoor humidity or zone specific 77 | humidity is not currently available via Nexia's published data. 78 | 79 | | Attribute type | Description | 80 | | -------------- | ----------- | 81 | | Float | current humidity | 82 | 83 | ### Attribute `current_temperature` 84 | 85 | Provides you with the current zone's temperature. 86 | 87 | | Attribute type | Description | 88 | | -------------- | ----------- | 89 | | Integer | current temperature | 90 | 91 | ### Attribute `fan_list` 92 | 93 | This is a list of all available fan modes you can select, separated by commas. 94 | 95 | | Attribute type | Description | 96 | | -------------- | ----------- | 97 | | String | 'auto,on,circulate' | 98 | 99 | ### Attribute `fan_mode` 100 | 101 | The currently selected fan mode. 102 | 103 | | Attribute type | Description | 104 | | -------------- | ----------- | 105 | | String | 'auto', 'on', or 'circulate' | 106 | 107 | ### Attribute `firmware` 108 | 109 | Provides you with the current firmware version of the main thermostat. 110 | 111 | | Attribute type | Description | 112 | | -------------- | ----------- | 113 | | String | firmware version | 114 | 115 | ### Attribute `hold_mode` 116 | 117 | Indicates if a hold is currently in place on this zone. Examples of such are 'away', 118 | 'home', 'sleep', or 'evening' 119 | 120 | | Attribute type | Description | 121 | | -------------- | ----------- | 122 | | String | hold mode | 123 | 124 | ### Attribute `humidity` 125 | 126 | The target dehumidify set point (%) of the system. 127 | 128 | | Attribute type | Description | 129 | | -------------- | ----------- | 130 | | Integer | dehumidify setpoint as an integer | 131 | 132 | ### Attribute `humidify_supported` 133 | 134 | Indicates if the system supports humidification. 135 | 136 | | Attribute type | Description | 137 | | -------------- | ----------- | 138 | | Boolean | humidification supported | 139 | 140 | ### Attribute `dehumidify_supported` 141 | 142 | Indicates if the system supports dehumidification. 143 | 144 | | Attribute type | Description | 145 | | -------------- | ----------- | 146 | | Boolean | dehumidification supported | 147 | 148 | 149 | ### Attribute `humidify_setpoint` 150 | 151 | The target humidify set point (%) of the system. 152 | 153 | | Attribute type | Description | 154 | | -------------- | ----------- | 155 | | Integer | humidify setpoint as an integer | 156 | 157 | 158 | ### Attribute `dehumidify_setpoint` 159 | 160 | Same as `humidity` The target dehumidify set point (%) of the system. 161 | 162 | | Attribute type | Description | 163 | | -------------- | ----------- | 164 | | Integer | dehumidify setpoint as an integer | 165 | 166 | 167 | ### Attribute `max_humidity` 168 | 169 | Hard-coded value indicating the maximum dehumidify set point (%) you can set, as an integer. 170 | 171 | | Attribute type | Description | 172 | | -------------- | ----------- | 173 | | Integer | maximum humidity set point, always 65 | 174 | 175 | ### Attribute `max_temp` 176 | 177 | The maximum temperature set point of the zone. This can change based on the thermostat's settings. 178 | 179 | | Attribute type | Description | 180 | | -------------- | ----------- | 181 | | Integer | maximum temperature, such as 90 | 182 | 183 | ### Attribute `min_humidity` 184 | 185 | Hard-coded value indicating the minimum dehumidify set point (%) you can set, as an integer. 186 | 187 | | Attribute type | Description | 188 | | -------------- | ----------- | 189 | | Integer | minimum humidity set point, always 35 | 190 | 191 | ### Attribute `min_temp` 192 | 193 | The minimum temperature set point of the zone. This can change based on the thermostat's settings. 194 | 195 | | Attribute type | Description | 196 | | -------------- | ----------- | 197 | | Integer | minimum temperature, such as 55 | 198 | 199 | ### Attribute `model` 200 | 201 | The thermostat model, such as 'TZON1050AC52ZAA' 202 | 203 | | Attribute type | Description | 204 | | -------------- | ----------- | 205 | | String | thermostat model | 206 | 207 | ### Attribute `operation_list` 208 | 209 | List of available operation modes such as 'AUTO,COOL,HEAT,OFF' 210 | 211 | | Attribute type | Description | 212 | | -------------- | ----------- | 213 | | String | operation modes | 214 | 215 | ### Attribute `operation_mode` 216 | 217 | The current operation mode, such as 'AUTO', 'COOL', 'HEAT', or 'OFF' 218 | 219 | | Attribute type | Description | 220 | | -------------- | ----------- | 221 | | String | operation mode | 222 | 223 | ### Attribute `setpoint_status` 224 | 225 | This provides you with a system set point status, such as 'Holding Permanently', 226 | 'Following Schedule - Away', or 'Following Schedule - Home'. This is not an exhaustive list. 227 | 228 | | Attribute type | Description | 229 | | -------------- | ----------- | 230 | | String | set point status | 231 | 232 | ### Attribute `target_temp_high` 233 | 234 | The target cooling (upper-bound) temperature for the current zone. 235 | 236 | | Attribute type | Description | 237 | | -------------- | ----------- | 238 | | Integer | upper-bound target temperature | 239 | 240 | ### Attribute `target_temp_low` 241 | 242 | The target heating (lower-bound) temperature for the current zone. 243 | 244 | | Attribute type | Description | 245 | | -------------- | ----------- | 246 | | Integer | lower-bound target temperature | 247 | 248 | ### Attribute `target_temp_step` 249 | 250 | The step at which the temperature can be increased or decreased. For Fahrenheit, 251 | this is 1.0 degrees per step, and for Celsius, this is 0.5 degrees per step. 252 | 253 | | Attribute type | Description | 254 | | -------------- | ----------- | 255 | | Float | step | 256 | 257 | ### Attribute `temperature` 258 | 259 | Based on the current system mode, this is the target temperature for the zone. 260 | 261 | | Attribute type | Description | 262 | | -------------- | ----------- | 263 | | Integer | target temperature | 264 | 265 | ### Attribute `thermostat_id` 266 | 267 | This is the main thermostat's ID, here for reference. This will match up with the 'id' 268 | in the JSON data provided by Nexia. 269 | 270 | | Attribute type | Description | 271 | | -------------- | ----------- | 272 | | Integer | thermostat ID | 273 | 274 | ### Attribute `thermostat_name` 275 | 276 | The name of the system. This will be shared across all zones of your Trane / American Standard 277 | system. 278 | 279 | | Attribute type | Description | 280 | | -------------- | ----------- | 281 | | String | thermostat name| 282 | 283 | ### Attribute `zone_id` 284 | 285 | The zone ID for this particular zone, here for reference. This will match up with the 'id' 286 | in the JSON data provided by Nexia under the 'zones' list. 287 | 288 | | Attribute type | Description | 289 | | -------------- | ----------- | 290 | | Integer | zone ID | 291 | 292 | ### Attribute `zone_status` 293 | 294 | The status of the zone, such as 'Cooling', or 'Heating" 295 | 296 | | Attribute type | Description | 297 | | -------------- | ----------- | 298 | | String | zone status | 299 | 300 | 301 | ## Services 302 | 303 | The following `climate` services are provided by the Nexia Thermostat: 304 | `set_aux_heat`, `set_away_mode`, `set_fan_mode`, `set_hold_mode`, `set_humidity`, 305 | `set_operation_mode`, `set_temperature`, `turn_on`, `turn_off` 306 | 307 | The service `set_swing_mode` offered by the [Climate component](/components/climate/) 308 | is not implemented for this thermostat. 309 | 310 | The following `nexia` climate service is provided by the Nexia Thermostat: 311 | `set_aircleaner_mode` 312 | 313 | ### Service `set_aux_heat` 314 | 315 | Enable the aux / emergency heat for the system. This is a system-wide setting. 316 | 317 | | Service data attribute | Optional | Description | 318 | | ---------------------- | -------- | ----------- | 319 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 320 | | `away_mode` | no | 'on' or 'off' 321 | 322 | ### Service `set_away_mode` 323 | 324 | Turns the away mode on or off for the thermostat. 325 | 326 | | Service data attribute | Optional | Description | 327 | | ---------------------- | -------- | ----------- | 328 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 329 | | `away_mode` | no | 'on' or 'off' 330 | 331 | ### Service `set_fan_mode` 332 | 333 | Sets the fan mode for the system. See the `fan_list` attribute for options. This is a system-wide setting. 334 | 335 | | Service data attribute | Optional | Description | 336 | | ---------------------- | -------- | ----------- | 337 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 338 | | `fan_mode` | no | 'auto', 'on', or 'circulate' 339 | 340 | 341 | ### Service `set_hold_mode` 342 | 343 | Puts the thermostat into the given hold mode. For 'home', 'away', 'sleep', 344 | and any other hold based on a reference climate, the 345 | target temperature is taken from the reference climate. 346 | 347 | | Service data attribute | Optional | Description | 348 | | ---------------------- | -------- | ----------- | 349 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 350 | | `hold_mode` | no | `home`, `away`, `sleep` 351 | 352 | ### Service `set_humidity` 353 | 354 | Sets the dehumidify set point of the system. Range from 35-65. This is a system-wide setting. 355 | 356 | | Service data attribute | Optional | Description | 357 | | ---------------------- | -------- | ----------- | 358 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 359 | | `humidity` | no | The dehumidify setpoint, like 50. 360 | 361 | ### Service `set_temperature` 362 | 363 | Puts the thermostat into a temporary hold at the given temperature. 364 | 365 | | Service data attribute | Optional | Description | 366 | | ---------------------- | -------- | ----------- | 367 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 368 | | `target_temp_low` | no | Desired heating target temperature (when in auto mode) 369 | | `target_temp_high` | no | Desired cooling target temperature (when in auto mode) 370 | | `temperature` | no | Desired target temperature (when not in auto mode) 371 | 372 | Only the target temperatures relevant for the current operation mode need to 373 | be provided. 374 | 375 | ### Service `set_operation_mode` 376 | 377 | Sets the current operation mode of the thermostat. See attribute `operation_list` for options. 378 | 379 | | Service data attribute | Optional | Description | 380 | | ---------------------- | -------- | ----------- | 381 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 382 | | `operation_mode` | no | 'AUTO', 'COOL', 'HEAT', or 'OFF' 383 | 384 | ### Service `turn_on` 385 | 386 | Turns the zone on. 387 | 388 | | Service data attribute | Optional | Description | 389 | | ---------------------- | -------- | ----------- | 390 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 391 | 392 | ### Service `turn_off` 393 | 394 | Turns the zone off. 395 | 396 | | Service data attribute | Optional | Description | 397 | | ---------------------- | -------- | ----------- | 398 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 399 | 400 | ### Service `set_aircleaner_mode` 401 | 402 | Part of the `nexia.` services. Sets the air cleaner mode. Options include 'AUTO', 'QUICK', and 403 | 'ALLERGY'. This is a system-wide setting. 404 | 405 | | Service data attribute | Optional | Description | 406 | | ---------------------- | -------- | ----------- | 407 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 408 | | `aircleaner_mode` | no | 'AUTO', 'QUICK', or 'ALLERGY' 409 | 410 | ### Service `set_humidify_setpoint` 411 | 412 | Part of the `nexia.` services. Sets the humidify setpoint. This is a system-wide setting. 413 | 414 | | Service data attribute | Optional | Description | 415 | | ---------------------- | -------- | ----------- | 416 | | `entity_id` | yes | String or list of strings that point at `entity_id`'s of climate devices to control. Else targets all. 417 | | `humidity` | no | Humidify setpoint level, from 35 to 65. 418 | -------------------------------------------------------------------------------- /custom_components/nexia/climate.py: -------------------------------------------------------------------------------- 1 | """Support for Nexia / Trane XL thermostats.""" 2 | import logging 3 | 4 | from nexia.const import ( 5 | FAN_MODES, 6 | OPERATION_MODE_AUTO, 7 | OPERATION_MODE_COOL, 8 | OPERATION_MODE_HEAT, 9 | OPERATION_MODE_OFF, 10 | SYSTEM_STATUS_COOL, 11 | SYSTEM_STATUS_HEAT, 12 | SYSTEM_STATUS_IDLE, 13 | UNIT_FAHRENHEIT, 14 | ) 15 | import voluptuous as vol 16 | 17 | from homeassistant.components.climate import ClimateDevice 18 | from homeassistant.components.climate.const import ( 19 | ATTR_HUMIDITY, 20 | ATTR_MAX_HUMIDITY, 21 | ATTR_MIN_HUMIDITY, 22 | ATTR_TARGET_TEMP_HIGH, 23 | ATTR_TARGET_TEMP_LOW, 24 | CURRENT_HVAC_COOL, 25 | CURRENT_HVAC_HEAT, 26 | CURRENT_HVAC_IDLE, 27 | CURRENT_HVAC_OFF, 28 | HVAC_MODE_AUTO, 29 | HVAC_MODE_COOL, 30 | HVAC_MODE_HEAT, 31 | HVAC_MODE_HEAT_COOL, 32 | HVAC_MODE_OFF, 33 | SUPPORT_AUX_HEAT, 34 | SUPPORT_FAN_MODE, 35 | SUPPORT_PRESET_MODE, 36 | SUPPORT_TARGET_HUMIDITY, 37 | SUPPORT_TARGET_TEMPERATURE, 38 | SUPPORT_TARGET_TEMPERATURE_RANGE, 39 | ) 40 | from homeassistant.const import ( 41 | ATTR_ENTITY_ID, 42 | ATTR_TEMPERATURE, 43 | TEMP_CELSIUS, 44 | TEMP_FAHRENHEIT, 45 | ) 46 | from homeassistant.helpers import entity_platform 47 | import homeassistant.helpers.config_validation as cv 48 | from homeassistant.helpers.dispatcher import dispatcher_send 49 | 50 | from .const import ( 51 | ATTR_AIRCLEANER_MODE, 52 | ATTR_DEHUMIDIFY_SETPOINT, 53 | ATTR_DEHUMIDIFY_SUPPORTED, 54 | ATTR_HUMIDIFY_SETPOINT, 55 | ATTR_HUMIDIFY_SUPPORTED, 56 | ATTR_ZONE_STATUS, 57 | DOMAIN, 58 | NEXIA_DEVICE, 59 | SIGNAL_THERMOSTAT_UPDATE, 60 | SIGNAL_ZONE_UPDATE, 61 | UPDATE_COORDINATOR, 62 | ) 63 | from .entity import NexiaThermostatZoneEntity 64 | from .util import percent_conv 65 | 66 | SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" 67 | SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" 68 | 69 | SET_AIRCLEANER_SCHEMA = vol.Schema( 70 | { 71 | vol.Required(ATTR_ENTITY_ID): cv.entity_ids, 72 | vol.Required(ATTR_AIRCLEANER_MODE): cv.string, 73 | } 74 | ) 75 | 76 | SET_HUMIDITY_SCHEMA = vol.Schema( 77 | { 78 | vol.Required(ATTR_ENTITY_ID): cv.entity_ids, 79 | vol.Required(ATTR_HUMIDITY): vol.All( 80 | vol.Coerce(int), vol.Range(min=35, max=65) 81 | ), 82 | } 83 | ) 84 | 85 | 86 | _LOGGER = logging.getLogger(__name__) 87 | 88 | # 89 | # Nexia has two bits to determine hvac mode 90 | # There are actually eight states so we map to 91 | # the most significant state 92 | # 93 | # 1. Zone Mode : Auto / Cooling / Heating / Off 94 | # 2. Run Mode : Hold / Run Schedule 95 | # 96 | # 97 | HA_TO_NEXIA_HVAC_MODE_MAP = { 98 | HVAC_MODE_HEAT: OPERATION_MODE_HEAT, 99 | HVAC_MODE_COOL: OPERATION_MODE_COOL, 100 | HVAC_MODE_HEAT_COOL: OPERATION_MODE_AUTO, 101 | HVAC_MODE_AUTO: OPERATION_MODE_AUTO, 102 | HVAC_MODE_OFF: OPERATION_MODE_OFF, 103 | } 104 | NEXIA_TO_HA_HVAC_MODE_MAP = { 105 | value: key for key, value in HA_TO_NEXIA_HVAC_MODE_MAP.items() 106 | } 107 | 108 | 109 | async def async_setup_entry(hass, config_entry, async_add_entities): 110 | """Set up climate for a Nexia device.""" 111 | 112 | nexia_data = hass.data[DOMAIN][config_entry.entry_id] 113 | nexia_home = nexia_data[NEXIA_DEVICE] 114 | coordinator = nexia_data[UPDATE_COORDINATOR] 115 | 116 | platform = entity_platform.current_platform.get() 117 | 118 | platform.async_register_entity_service( 119 | SERVICE_SET_HUMIDIFY_SETPOINT, 120 | SET_HUMIDITY_SCHEMA, 121 | SERVICE_SET_HUMIDIFY_SETPOINT, 122 | ) 123 | platform.async_register_entity_service( 124 | SERVICE_SET_AIRCLEANER_MODE, SET_AIRCLEANER_SCHEMA, SERVICE_SET_AIRCLEANER_MODE 125 | ) 126 | 127 | entities = [] 128 | for thermostat_id in nexia_home.get_thermostat_ids(): 129 | thermostat = nexia_home.get_thermostat_by_id(thermostat_id) 130 | for zone_id in thermostat.get_zone_ids(): 131 | zone = thermostat.get_zone_by_id(zone_id) 132 | entities.append(NexiaZone(coordinator, zone)) 133 | 134 | async_add_entities(entities, True) 135 | 136 | 137 | class NexiaZone(NexiaThermostatZoneEntity, ClimateDevice): 138 | """Provides Nexia Climate support.""" 139 | 140 | def __init__(self, coordinator, zone): 141 | """Initialize the thermostat.""" 142 | super().__init__( 143 | coordinator, zone, name=zone.get_name(), unique_id=zone.zone_id 144 | ) 145 | self._undo_humidfy_dispatcher = None 146 | self._undo_aircleaner_dispatcher = None 147 | # The has_* calls are stable for the life of the device 148 | # and do not do I/O 149 | self._has_relative_humidity = self._thermostat.has_relative_humidity() 150 | self._has_emergency_heat = self._thermostat.has_emergency_heat() 151 | self._has_humidify_support = self._thermostat.has_humidify_support() 152 | self._has_dehumidify_support = self._thermostat.has_dehumidify_support() 153 | 154 | @property 155 | def supported_features(self): 156 | """Return the list of supported features.""" 157 | supported = ( 158 | SUPPORT_TARGET_TEMPERATURE_RANGE 159 | | SUPPORT_TARGET_TEMPERATURE 160 | | SUPPORT_FAN_MODE 161 | | SUPPORT_PRESET_MODE 162 | ) 163 | 164 | if self._has_humidify_support or self._has_dehumidify_support: 165 | supported |= SUPPORT_TARGET_HUMIDITY 166 | 167 | if self._has_emergency_heat: 168 | supported |= SUPPORT_AUX_HEAT 169 | 170 | return supported 171 | 172 | @property 173 | def is_fan_on(self): 174 | """Blower is on.""" 175 | return self._thermostat.is_blower_active() 176 | 177 | @property 178 | def temperature_unit(self): 179 | """Return the unit of measurement.""" 180 | return TEMP_CELSIUS if self._thermostat.get_unit() == "C" else TEMP_FAHRENHEIT 181 | 182 | @property 183 | def current_temperature(self): 184 | """Return the current temperature.""" 185 | return self._zone.get_temperature() 186 | 187 | @property 188 | def fan_mode(self): 189 | """Return the fan setting.""" 190 | return self._thermostat.get_fan_mode() 191 | 192 | @property 193 | def fan_modes(self): 194 | """Return the list of available fan modes.""" 195 | return FAN_MODES 196 | 197 | @property 198 | def min_temp(self): 199 | """Minimum temp for the current setting.""" 200 | return (self._thermostat.get_setpoint_limits())[0] 201 | 202 | @property 203 | def max_temp(self): 204 | """Maximum temp for the current setting.""" 205 | return (self._thermostat.get_setpoint_limits())[1] 206 | 207 | def set_fan_mode(self, fan_mode): 208 | """Set new target fan mode.""" 209 | self._thermostat.set_fan_mode(fan_mode) 210 | self._signal_thermostat_update() 211 | 212 | @property 213 | def preset_mode(self): 214 | """Preset that is active.""" 215 | return self._zone.get_preset() 216 | 217 | @property 218 | def preset_modes(self): 219 | """All presets.""" 220 | return self._zone.get_presets() 221 | 222 | def set_humidity(self, humidity): 223 | """Dehumidify target.""" 224 | self._thermostat.set_dehumidify_setpoint(humidity / 100.0) 225 | self._signal_thermostat_update() 226 | 227 | @property 228 | def target_humidity(self): 229 | """Humidity indoors setpoint.""" 230 | if self._has_dehumidify_support: 231 | return percent_conv(self._thermostat.get_dehumidify_setpoint()) 232 | if self._has_humidify_support: 233 | return percent_conv(self._thermostat.get_humidify_setpoint()) 234 | return None 235 | 236 | @property 237 | def current_humidity(self): 238 | """Humidity indoors.""" 239 | if self._has_relative_humidity: 240 | return percent_conv(self._thermostat.get_relative_humidity()) 241 | return None 242 | 243 | @property 244 | def target_temperature(self): 245 | """Temperature we try to reach.""" 246 | current_mode = self._zone.get_current_mode() 247 | 248 | if current_mode == OPERATION_MODE_COOL: 249 | return self._zone.get_cooling_setpoint() 250 | if current_mode == OPERATION_MODE_HEAT: 251 | return self._zone.get_heating_setpoint() 252 | return None 253 | 254 | @property 255 | def target_temperature_step(self): 256 | """Step size of temperature units.""" 257 | if self._thermostat.get_unit() == UNIT_FAHRENHEIT: 258 | return 1.0 259 | return 0.5 260 | 261 | @property 262 | def target_temperature_high(self): 263 | """Highest temperature we are trying to reach.""" 264 | current_mode = self._zone.get_current_mode() 265 | 266 | if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT): 267 | return None 268 | return self._zone.get_cooling_setpoint() 269 | 270 | @property 271 | def target_temperature_low(self): 272 | """Lowest temperature we are trying to reach.""" 273 | current_mode = self._zone.get_current_mode() 274 | 275 | if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT): 276 | return None 277 | return self._zone.get_heating_setpoint() 278 | 279 | @property 280 | def hvac_action(self) -> str: 281 | """Operation ie. heat, cool, idle.""" 282 | system_status = self._thermostat.get_system_status() 283 | zone_called = self._zone.is_calling() 284 | 285 | if self._zone.get_requested_mode() == OPERATION_MODE_OFF: 286 | return CURRENT_HVAC_OFF 287 | if not zone_called: 288 | return CURRENT_HVAC_IDLE 289 | if system_status == SYSTEM_STATUS_COOL: 290 | return CURRENT_HVAC_COOL 291 | if system_status == SYSTEM_STATUS_HEAT: 292 | return CURRENT_HVAC_HEAT 293 | if system_status == SYSTEM_STATUS_IDLE: 294 | return CURRENT_HVAC_IDLE 295 | return CURRENT_HVAC_IDLE 296 | 297 | @property 298 | def hvac_mode(self): 299 | """Return current mode, as the user-visible name.""" 300 | mode = self._zone.get_requested_mode() 301 | hold = self._zone.is_in_permanent_hold() 302 | 303 | # If the device is in hold mode with 304 | # OPERATION_MODE_AUTO 305 | # overriding the schedule by still 306 | # heating and cooling to the 307 | # temp range. 308 | if hold and mode == OPERATION_MODE_AUTO: 309 | return HVAC_MODE_HEAT_COOL 310 | 311 | return NEXIA_TO_HA_HVAC_MODE_MAP[mode] 312 | 313 | @property 314 | def hvac_modes(self): 315 | """List of HVAC available modes.""" 316 | return [ 317 | HVAC_MODE_OFF, 318 | HVAC_MODE_AUTO, 319 | HVAC_MODE_HEAT_COOL, 320 | HVAC_MODE_HEAT, 321 | HVAC_MODE_COOL, 322 | ] 323 | 324 | def set_temperature(self, **kwargs): 325 | """Set target temperature.""" 326 | new_heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) 327 | new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) 328 | set_temp = kwargs.get(ATTR_TEMPERATURE) 329 | 330 | deadband = self._thermostat.get_deadband() 331 | cur_cool_temp = self._zone.get_cooling_setpoint() 332 | cur_heat_temp = self._zone.get_heating_setpoint() 333 | (min_temp, max_temp) = self._thermostat.get_setpoint_limits() 334 | 335 | # Check that we're not going to hit any minimum or maximum values 336 | if new_heat_temp and new_heat_temp + deadband > max_temp: 337 | new_heat_temp = max_temp - deadband 338 | if new_cool_temp and new_cool_temp - deadband < min_temp: 339 | new_cool_temp = min_temp + deadband 340 | 341 | # Check that we're within the deadband range, fix it if we're not 342 | if new_heat_temp and new_heat_temp != cur_heat_temp: 343 | if new_cool_temp - new_heat_temp < deadband: 344 | new_cool_temp = new_heat_temp + deadband 345 | if new_cool_temp and new_cool_temp != cur_cool_temp: 346 | if new_cool_temp - new_heat_temp < deadband: 347 | new_heat_temp = new_cool_temp - deadband 348 | 349 | self._zone.set_heat_cool_temp( 350 | heat_temperature=new_heat_temp, 351 | cool_temperature=new_cool_temp, 352 | set_temperature=set_temp, 353 | ) 354 | self._signal_zone_update() 355 | 356 | @property 357 | def is_aux_heat(self): 358 | """Emergency heat state.""" 359 | return self._thermostat.is_emergency_heat_active() 360 | 361 | @property 362 | def device_state_attributes(self): 363 | """Return the device specific state attributes.""" 364 | data = super().device_state_attributes 365 | 366 | data[ATTR_ZONE_STATUS] = self._zone.get_status() 367 | 368 | if not self._has_relative_humidity: 369 | return data 370 | 371 | min_humidity = percent_conv(self._thermostat.get_humidity_setpoint_limits()[0]) 372 | max_humidity = percent_conv(self._thermostat.get_humidity_setpoint_limits()[1]) 373 | data.update( 374 | { 375 | ATTR_MIN_HUMIDITY: min_humidity, 376 | ATTR_MAX_HUMIDITY: max_humidity, 377 | ATTR_DEHUMIDIFY_SUPPORTED: self._has_dehumidify_support, 378 | ATTR_HUMIDIFY_SUPPORTED: self._has_humidify_support, 379 | } 380 | ) 381 | 382 | if self._has_dehumidify_support: 383 | dehumdify_setpoint = percent_conv( 384 | self._thermostat.get_dehumidify_setpoint() 385 | ) 386 | data[ATTR_DEHUMIDIFY_SETPOINT] = dehumdify_setpoint 387 | 388 | if self._has_humidify_support: 389 | humdify_setpoint = percent_conv(self._thermostat.get_humidify_setpoint()) 390 | data[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint 391 | 392 | return data 393 | 394 | def set_preset_mode(self, preset_mode: str): 395 | """Set the preset mode.""" 396 | self._zone.set_preset(preset_mode) 397 | self._signal_zone_update() 398 | 399 | def turn_aux_heat_off(self): 400 | """Turn. Aux Heat off.""" 401 | self._thermostat.set_emergency_heat(False) 402 | self._signal_thermostat_update() 403 | 404 | def turn_aux_heat_on(self): 405 | """Turn. Aux Heat on.""" 406 | self._thermostat.set_emergency_heat(True) 407 | self._signal_thermostat_update() 408 | 409 | def turn_off(self): 410 | """Turn. off the zone.""" 411 | self.set_hvac_mode(OPERATION_MODE_OFF) 412 | self._signal_zone_update() 413 | 414 | def turn_on(self): 415 | """Turn. on the zone.""" 416 | self.set_hvac_mode(OPERATION_MODE_AUTO) 417 | self._signal_zone_update() 418 | 419 | def set_hvac_mode(self, hvac_mode: str) -> None: 420 | """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" 421 | if hvac_mode == HVAC_MODE_AUTO: 422 | self._zone.call_return_to_schedule() 423 | self._zone.set_mode(mode=OPERATION_MODE_AUTO) 424 | else: 425 | self._zone.call_permanent_hold() 426 | self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) 427 | 428 | self.schedule_update_ha_state() 429 | 430 | def set_aircleaner_mode(self, aircleaner_mode): 431 | """Set the aircleaner mode.""" 432 | self._thermostat.set_air_cleaner(aircleaner_mode) 433 | self._signal_thermostat_update() 434 | 435 | def set_humidify_setpoint(self, humidity): 436 | """Set the humidify setpoint.""" 437 | self._thermostat.set_humidify_setpoint(humidity / 100.0) 438 | self._signal_thermostat_update() 439 | 440 | def _signal_thermostat_update(self): 441 | """Signal a thermostat update. 442 | 443 | Whenever the underlying library does an action against 444 | a thermostat, the data for the thermostat and all 445 | connected zone is updated. 446 | 447 | Update all the zones on the thermostat. 448 | """ 449 | dispatcher_send( 450 | self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}" 451 | ) 452 | 453 | def _signal_zone_update(self): 454 | """Signal a zone update. 455 | 456 | Whenever the underlying library does an action against 457 | a zone, the data for the zone is updated. 458 | 459 | Update a single zone. 460 | """ 461 | dispatcher_send(self.hass, f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}") 462 | 463 | async def async_update(self): 464 | """Update the entity. 465 | 466 | Only used by the generic entity update service. 467 | """ 468 | await self._coordinator.async_request_refresh() 469 | -------------------------------------------------------------------------------- /custom_components/nexia/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 101 | 102 | 103 | 104 | def set_ 105 | xx 106 | } 107 | get_presets 108 | set 109 | 302 110 | damper open 111 | zone_status 112 | damper 113 | damper ope 114 | 115 | 116 | , 117 | 118 | 119 | 120 | 134 | 135 | 136 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 |