Add support for nexia automations (#33049)
* Add support for nexia automations Bump nexia to 0.7.1 Start adding tests Fix some of the climate attributes that were wrong (discovered while adding tests) Pass the name of the instance so the nexia UI does not display "My Mobile" * fix mocking * faster asserts, scene * scene makes so much more sense * pylint * Update homeassistant/components/nexia/scene.py Co-Authored-By: Martin Hjelmare <marhje52@gmail.com> * docstring cleanup Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/33074/head
parent
836413a4a8
commit
85328399e0
|
@ -62,7 +62,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
|
||||
try:
|
||||
nexia_home = await hass.async_add_executor_job(
|
||||
partial(NexiaHome, username=username, password=password)
|
||||
partial(
|
||||
NexiaHome,
|
||||
username=username,
|
||||
password=password,
|
||||
device_name=hass.config.location_name,
|
||||
)
|
||||
)
|
||||
except ConnectTimeout as ex:
|
||||
_LOGGER.error("Unable to connect to Nexia service: %s", ex)
|
||||
|
|
|
@ -10,6 +10,7 @@ from nexia.const import (
|
|||
SYSTEM_STATUS_COOL,
|
||||
SYSTEM_STATUS_HEAT,
|
||||
SYSTEM_STATUS_IDLE,
|
||||
UNIT_FAHRENHEIT,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
|
@ -32,6 +33,7 @@ from homeassistant.components.climate.const import (
|
|||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
|
@ -119,7 +121,12 @@ class NexiaZone(NexiaEntity, ClimateDevice):
|
|||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
supported = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE
|
||||
supported = (
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
| SUPPORT_TARGET_TEMPERATURE
|
||||
| SUPPORT_FAN_MODE
|
||||
| SUPPORT_PRESET_MODE
|
||||
)
|
||||
|
||||
if self._has_humidify_support or self._has_dehumidify_support:
|
||||
supported |= SUPPORT_TARGET_HUMIDITY
|
||||
|
@ -159,6 +166,16 @@ class NexiaZone(NexiaEntity, ClimateDevice):
|
|||
"""Return the list of available fan modes."""
|
||||
return FAN_MODES
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Minimum temp for the current setting."""
|
||||
return (self._device.thermostat.get_setpoint_limits())[0]
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Maximum temp for the current setting."""
|
||||
return (self._device.thermostat.get_setpoint_limits())[1]
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
self.thermostat.set_fan_mode(fan_mode)
|
||||
|
@ -198,8 +215,37 @@ class NexiaZone(NexiaEntity, ClimateDevice):
|
|||
@property
|
||||
def target_temperature(self):
|
||||
"""Temperature we try to reach."""
|
||||
if self._device.get_current_mode() == "COOL":
|
||||
current_mode = self._device.get_current_mode()
|
||||
|
||||
if current_mode == OPERATION_MODE_COOL:
|
||||
return self._device.get_cooling_setpoint()
|
||||
if current_mode == OPERATION_MODE_HEAT:
|
||||
return self._device.get_heating_setpoint()
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Step size of temperature units."""
|
||||
if self._device.thermostat.get_unit() == UNIT_FAHRENHEIT:
|
||||
return 1.0
|
||||
return 0.5
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Highest temperature we are trying to reach."""
|
||||
current_mode = self._device.get_current_mode()
|
||||
|
||||
if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT):
|
||||
return None
|
||||
return self._device.get_cooling_setpoint()
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Lowest temperature we are trying to reach."""
|
||||
current_mode = self._device.get_current_mode()
|
||||
|
||||
if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT):
|
||||
return None
|
||||
return self._device.get_heating_setpoint()
|
||||
|
||||
@property
|
||||
|
|
|
@ -26,6 +26,7 @@ async def validate_input(hass: core.HomeAssistant, data):
|
|||
password=data[CONF_PASSWORD],
|
||||
auto_login=False,
|
||||
auto_update=False,
|
||||
device_name=hass.config.location_name,
|
||||
)
|
||||
await hass.async_add_executor_job(nexia_home.login)
|
||||
except ConnectTimeout as ex:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Nexia constants."""
|
||||
|
||||
PLATFORMS = ["sensor", "binary_sensor", "climate"]
|
||||
PLATFORMS = ["sensor", "binary_sensor", "climate", "scene"]
|
||||
|
||||
ATTRIBUTION = "Data provided by mynexia.com"
|
||||
|
||||
|
@ -14,6 +14,8 @@ NEXIA_SCAN_INTERVAL = "scan_interval"
|
|||
DOMAIN = "nexia"
|
||||
DEFAULT_ENTITY_NAMESPACE = "nexia"
|
||||
|
||||
ATTR_DESCRIPTION = "description"
|
||||
|
||||
ATTR_ZONE_STATUS = "zone_status"
|
||||
ATTR_HUMIDIFY_SUPPORTED = "humidify_supported"
|
||||
ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "nexia",
|
||||
"name": "Nexia",
|
||||
"requirements": [
|
||||
"nexia==0.4.1"
|
||||
"nexia==0.7.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
"""Support for Nexia Automations."""
|
||||
|
||||
from homeassistant.components.scene import Scene
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
|
||||
from .const import (
|
||||
ATTR_DESCRIPTION,
|
||||
ATTRIBUTION,
|
||||
DATA_NEXIA,
|
||||
DOMAIN,
|
||||
NEXIA_DEVICE,
|
||||
UPDATE_COORDINATOR,
|
||||
)
|
||||
from .entity import NexiaEntity
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up automations for a Nexia device."""
|
||||
|
||||
nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA]
|
||||
nexia_home = nexia_data[NEXIA_DEVICE]
|
||||
coordinator = nexia_data[UPDATE_COORDINATOR]
|
||||
entities = []
|
||||
|
||||
# Automation switches
|
||||
for automation_id in nexia_home.get_automation_ids():
|
||||
automation = nexia_home.get_automation_by_id(automation_id)
|
||||
|
||||
entities.append(NexiaAutomationScene(coordinator, automation))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class NexiaAutomationScene(NexiaEntity, Scene):
|
||||
"""Provides Nexia automation support."""
|
||||
|
||||
def __init__(self, coordinator, automation):
|
||||
"""Initialize the automation scene."""
|
||||
super().__init__(coordinator)
|
||||
self._automation = automation
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id of the automation scene."""
|
||||
# This is the automation unique_id
|
||||
return self._automation.automation_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the automation scene."""
|
||||
return self._automation.name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the scene specific state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
ATTR_DESCRIPTION: self._automation.description,
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the automation scene."""
|
||||
return "mdi:script-text-outline"
|
||||
|
||||
def activate(self):
|
||||
"""Activate an automation scene."""
|
||||
self._automation.activate()
|
|
@ -917,7 +917,7 @@ netdisco==2.6.0
|
|||
neurio==0.3.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.4.1
|
||||
nexia==0.7.1
|
||||
|
||||
# homeassistant.components.niko_home_control
|
||||
niko-home-control==0.2.1
|
||||
|
|
|
@ -344,7 +344,7 @@ nessclient==0.9.15
|
|||
netdisco==2.6.0
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.4.1
|
||||
nexia==0.7.1
|
||||
|
||||
# homeassistant.components.nsw_fuel_station
|
||||
nsw-fuel-api-client==1.0.10
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
"""The lock tests for the august platform."""
|
||||
|
||||
from homeassistant.components.climate.const import HVAC_MODE_HEAT_COOL
|
||||
|
||||
from .util import async_init_integration
|
||||
|
||||
|
||||
async def test_climate_zones(hass):
|
||||
"""Test creation climate zones."""
|
||||
|
||||
await async_init_integration(hass)
|
||||
|
||||
state = hass.states.get("climate.nick_office")
|
||||
assert state.state == HVAC_MODE_HEAT_COOL
|
||||
expected_attributes = {
|
||||
"attribution": "Data provided by mynexia.com",
|
||||
"current_humidity": 52.0,
|
||||
"current_temperature": 22.8,
|
||||
"dehumidify_setpoint": 45.0,
|
||||
"dehumidify_supported": True,
|
||||
"fan_mode": "auto",
|
||||
"fan_modes": ["auto", "on", "circulate"],
|
||||
"friendly_name": "Nick Office",
|
||||
"humidify_supported": False,
|
||||
"humidity": 45.0,
|
||||
"hvac_action": "cooling",
|
||||
"hvac_modes": ["off", "auto", "heat_cool", "heat", "cool"],
|
||||
"max_humidity": 65.0,
|
||||
"max_temp": 37.2,
|
||||
"min_humidity": 35.0,
|
||||
"min_temp": 12.8,
|
||||
"preset_mode": "None",
|
||||
"preset_modes": ["None", "Home", "Away", "Sleep"],
|
||||
"supported_features": 31,
|
||||
"target_temp_high": 26.1,
|
||||
"target_temp_low": 17.2,
|
||||
"target_temp_step": 1.0,
|
||||
"temperature": None,
|
||||
"zone_status": "Relieving Air",
|
||||
}
|
||||
# Only test for a subset of attributes in case
|
||||
# HA changes the implementation and a new one appears
|
||||
assert all(
|
||||
state.attributes[key] == expected_attributes[key] for key in expected_attributes
|
||||
)
|
|
@ -0,0 +1,72 @@
|
|||
"""The lock tests for the august platform."""
|
||||
|
||||
from .util import async_init_integration
|
||||
|
||||
|
||||
async def test_automation_scenees(hass):
|
||||
"""Test creation automation scenees."""
|
||||
|
||||
await async_init_integration(hass)
|
||||
|
||||
state = hass.states.get("scene.away_short")
|
||||
expected_attributes = {
|
||||
"attribution": "Data provided by mynexia.com",
|
||||
"description": "When IFTTT activates the automation Upstairs "
|
||||
"West Wing will permanently hold the heat to 63.0 "
|
||||
"and cool to 80.0 AND Downstairs East Wing will "
|
||||
"permanently hold the heat to 63.0 and cool to "
|
||||
"79.0 AND Downstairs West Wing will permanently "
|
||||
"hold the heat to 63.0 and cool to 79.0 AND "
|
||||
"Upstairs West Wing will permanently hold the "
|
||||
"heat to 63.0 and cool to 81.0 AND Upstairs West "
|
||||
"Wing will change Fan Mode to Auto AND Downstairs "
|
||||
"East Wing will change Fan Mode to Auto AND "
|
||||
"Downstairs West Wing will change Fan Mode to "
|
||||
"Auto AND Activate the mode named 'Away Short' "
|
||||
"AND Master Suite will permanently hold the heat "
|
||||
"to 63.0 and cool to 79.0 AND Master Suite will "
|
||||
"change Fan Mode to Auto",
|
||||
"friendly_name": "Away Short",
|
||||
"icon": "mdi:script-text-outline",
|
||||
}
|
||||
# Only test for a subset of attributes in case
|
||||
# HA changes the implementation and a new one appears
|
||||
assert all(
|
||||
state.attributes[key] == expected_attributes[key] for key in expected_attributes
|
||||
)
|
||||
|
||||
state = hass.states.get("scene.power_outage")
|
||||
expected_attributes = {
|
||||
"attribution": "Data provided by mynexia.com",
|
||||
"description": "When IFTTT activates the automation Upstairs "
|
||||
"West Wing will permanently hold the heat to 55.0 "
|
||||
"and cool to 90.0 AND Downstairs East Wing will "
|
||||
"permanently hold the heat to 55.0 and cool to "
|
||||
"90.0 AND Downstairs West Wing will permanently "
|
||||
"hold the heat to 55.0 and cool to 90.0 AND "
|
||||
"Activate the mode named 'Power Outage'",
|
||||
"friendly_name": "Power Outage",
|
||||
"icon": "mdi:script-text-outline",
|
||||
}
|
||||
# Only test for a subset of attributes in case
|
||||
# HA changes the implementation and a new one appears
|
||||
assert all(
|
||||
state.attributes[key] == expected_attributes[key] for key in expected_attributes
|
||||
)
|
||||
|
||||
state = hass.states.get("scene.power_restored")
|
||||
expected_attributes = {
|
||||
"attribution": "Data provided by mynexia.com",
|
||||
"description": "When IFTTT activates the automation Upstairs "
|
||||
"West Wing will Run Schedule AND Downstairs East "
|
||||
"Wing will Run Schedule AND Downstairs West Wing "
|
||||
"will Run Schedule AND Activate the mode named "
|
||||
"'Home'",
|
||||
"friendly_name": "Power Restored",
|
||||
"icon": "mdi:script-text-outline",
|
||||
}
|
||||
# Only test for a subset of attributes in case
|
||||
# HA changes the implementation and a new one appears
|
||||
assert all(
|
||||
state.attributes[key] == expected_attributes[key] for key in expected_attributes
|
||||
)
|
|
@ -0,0 +1,45 @@
|
|||
"""Tests for the nexia integration."""
|
||||
import uuid
|
||||
|
||||
from asynctest import patch
|
||||
from nexia.home import NexiaHome
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.nexia.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
async def async_init_integration(
|
||||
hass: HomeAssistant, skip_setup: bool = False,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the nexia integration in Home Assistant."""
|
||||
|
||||
house_fixture = "nexia/mobile_houses_123456.json"
|
||||
session_fixture = "nexia/session_123456.json"
|
||||
sign_in_fixture = "nexia/sign_in.json"
|
||||
|
||||
with requests_mock.mock() as m, patch(
|
||||
"nexia.home.load_or_create_uuid", return_value=uuid.uuid4()
|
||||
):
|
||||
m.post(NexiaHome.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture))
|
||||
m.get(
|
||||
NexiaHome.API_MOBILE_HOUSES_URL.format(house_id=123456),
|
||||
text=load_fixture(house_fixture),
|
||||
)
|
||||
m.post(
|
||||
NexiaHome.API_MOBILE_ACCOUNTS_SIGN_IN_URL,
|
||||
text=load_fixture(sign_in_fixture),
|
||||
)
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
if not skip_setup:
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"success" : true,
|
||||
"result" : {
|
||||
"is_activated_by_activation_code" : 0,
|
||||
"can_receive_notifications" : true,
|
||||
"can_manage_locks" : true,
|
||||
"can_control_automations" : true,
|
||||
"_links" : {
|
||||
"child" : [
|
||||
{
|
||||
"data" : {
|
||||
"name" : "House",
|
||||
"postal_code" : "12345",
|
||||
"id" : 123456
|
||||
}
|
||||
}
|
||||
],
|
||||
"self" : {
|
||||
"href" : "https://www.mynexia.com/mobile/session"
|
||||
}
|
||||
},
|
||||
"can_view_videos" : true
|
||||
},
|
||||
"error" : null
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"success": true,
|
||||
"error": null,
|
||||
"result": {
|
||||
"mobile_id": 1,
|
||||
"api_key": "mock",
|
||||
"setup_step": "done",
|
||||
"locale": "en_us"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue