Skip to content

Commit

Permalink
Add support for nexia automations (home-assistant#33049)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* docstring cleanup

Co-authored-by: Martin Hjelmare <[email protected]>
  • Loading branch information
bdraco and MartinHjelmare authored Mar 20, 2020
1 parent 836413a commit 8532839
Show file tree
Hide file tree
Showing 14 changed files with 8,362 additions and 7 deletions.
7 changes: 6 additions & 1 deletion homeassistant/components/nexia/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
50 changes: 48 additions & 2 deletions homeassistant/components/nexia/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
SYSTEM_STATUS_COOL,
SYSTEM_STATUS_HEAT,
SYSTEM_STATUS_IDLE,
UNIT_FAHRENHEIT,
)

from homeassistant.components.climate import ClimateDevice
Expand All @@ -32,6 +33,7 @@
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_HUMIDITY,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.const import (
ATTR_ATTRIBUTION,
Expand Down Expand Up @@ -119,7 +121,12 @@ def unique_id(self):
@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
Expand Down Expand Up @@ -159,6 +166,16 @@ def fan_modes(self):
"""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)
Expand Down Expand Up @@ -198,8 +215,37 @@ def current_humidity(self):
@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
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/nexia/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/nexia/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Nexia constants."""

PLATFORMS = ["sensor", "binary_sensor", "climate"]
PLATFORMS = ["sensor", "binary_sensor", "climate", "scene"]

ATTRIBUTION = "Data provided by mynexia.com"

Expand All @@ -14,6 +14,8 @@
DOMAIN = "nexia"
DEFAULT_ENTITY_NAMESPACE = "nexia"

ATTR_DESCRIPTION = "description"

ATTR_ZONE_STATUS = "zone_status"
ATTR_HUMIDIFY_SUPPORTED = "humidify_supported"
ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/nexia/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"domain": "nexia",
"name": "Nexia",
"requirements": [
"nexia==0.4.1"
"nexia==0.7.1"
],
"dependencies": [],
"codeowners": [
Expand Down
68 changes: 68 additions & 0 deletions homeassistant/components/nexia/scene.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/components/nexia/test_climate.py
Original file line number Diff line number Diff line change
@@ -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
)
72 changes: 72 additions & 0 deletions tests/components/nexia/test_scene.py
Original file line number Diff line number Diff line change
@@ -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
)
45 changes: 45 additions & 0 deletions tests/components/nexia/util.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 8532839

Please sign in to comment.