Add binary sensor for myq gateway connectivity (#33423)

This commit is contained in:
J. Nick Koston 2020-03-31 13:58:44 -05:00 committed by GitHub
parent 12b408219e
commit b783aab41b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 397 additions and 3 deletions

View File

@ -442,7 +442,6 @@ omit =
homeassistant/components/mychevy/*
homeassistant/components/mycroft/*
homeassistant/components/mycroft/notify.py
homeassistant/components/myq/cover.py
homeassistant/components/mysensors/*
homeassistant/components/mystrom/binary_sensor.py
homeassistant/components/mystrom/light.py

View File

@ -0,0 +1,108 @@
"""Support for MyQ gateways."""
import logging
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
BinarySensorDevice,
)
from homeassistant.core import callback
from .const import (
DOMAIN,
KNOWN_MODELS,
MANUFACTURER,
MYQ_COORDINATOR,
MYQ_DEVICE_FAMILY,
MYQ_DEVICE_FAMILY_GATEWAY,
MYQ_DEVICE_STATE,
MYQ_DEVICE_STATE_ONLINE,
MYQ_GATEWAY,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up mysq covers."""
data = hass.data[DOMAIN][config_entry.entry_id]
myq = data[MYQ_GATEWAY]
coordinator = data[MYQ_COORDINATOR]
entities = []
for device in myq.devices.values():
if device.device_json[MYQ_DEVICE_FAMILY] == MYQ_DEVICE_FAMILY_GATEWAY:
entities.append(MyQBinarySensorDevice(coordinator, device))
async_add_entities(entities, True)
class MyQBinarySensorDevice(BinarySensorDevice):
"""Representation of a MyQ gateway."""
def __init__(self, coordinator, device):
"""Initialize with API object, device id."""
self._coordinator = coordinator
self._device = device
@property
def device_class(self):
"""We track connectivity for gateways."""
return DEVICE_CLASS_CONNECTIVITY
@property
def name(self):
"""Return the name of the garage door if any."""
return f"{self._device.name} MyQ Gateway"
@property
def is_on(self):
"""Return if the device is online."""
if not self._coordinator.last_update_success:
return False
# Not all devices report online so assume True if its missing
return self._device.device_json[MYQ_DEVICE_STATE].get(
MYQ_DEVICE_STATE_ONLINE, True
)
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self._device.device_id
async def async_update(self):
"""Update status of cover."""
await self._coordinator.async_request_refresh()
@property
def device_info(self):
"""Return the device_info of the device."""
device_info = {
"identifiers": {(DOMAIN, self._device.device_id)},
"name": self.name,
"manufacturer": MANUFACTURER,
"sw_version": self._device.firmware_version,
}
model = KNOWN_MODELS.get(self._device.device_id[2:4])
if model:
device_info["model"] = model
return device_info
@property
def should_poll(self):
"""Return False, updates are controlled via coordinator."""
return False
@callback
def _async_consume_update(self):
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Subscribe to updates."""
self._coordinator.async_add_listener(self._async_consume_update)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
self._coordinator.async_remove_listener(self._async_consume_update)

View File

@ -10,10 +10,14 @@ from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_O
DOMAIN = "myq"
PLATFORMS = ["cover"]
PLATFORMS = ["cover", "binary_sensor"]
MYQ_DEVICE_TYPE = "device_type"
MYQ_DEVICE_TYPE_GATE = "gate"
MYQ_DEVICE_FAMILY = "device_family"
MYQ_DEVICE_FAMILY_GATEWAY = "gateway"
MYQ_DEVICE_STATE = "state"
MYQ_DEVICE_STATE_ONLINE = "online"
@ -39,3 +43,36 @@ TRANSITION_START_DURATION = 7
# Estimated time it takes myq to complete a transition
# from one state to another
TRANSITION_COMPLETE_DURATION = 37
MANUFACTURER = "The Chamberlain Group Inc."
KNOWN_MODELS = {
"00": "Chamberlain Ethernet Gateway",
"01": "LiftMaster Ethernet Gateway",
"02": "Craftsman Ethernet Gateway",
"03": "Chamberlain Wi-Fi hub",
"04": "LiftMaster Wi-Fi hub",
"05": "Craftsman Wi-Fi hub",
"08": "LiftMaster Wi-Fi GDO DC w/Battery Backup",
"09": "Chamberlain Wi-Fi GDO DC w/Battery Backup",
"10": "Craftsman Wi-Fi GDO DC 3/4HP",
"11": "MyQ Replacement Logic Board Wi-Fi GDO DC 3/4HP",
"12": "Chamberlain Wi-Fi GDO DC 1.25HP",
"13": "LiftMaster Wi-Fi GDO DC 1.25HP",
"14": "Craftsman Wi-Fi GDO DC 1.25HP",
"15": "MyQ Replacement Logic Board Wi-Fi GDO DC 1.25HP",
"0A": "Chamberlain Wi-Fi GDO or Gate Operator AC",
"0B": "LiftMaster Wi-Fi GDO or Gate Operator AC",
"0C": "Craftsman Wi-Fi GDO or Gate Operator AC",
"0D": "MyQ Replacement Logic Board Wi-Fi GDO or Gate Operator AC",
"0E": "Chamberlain Wi-Fi GDO DC 3/4HP",
"0F": "LiftMaster Wi-Fi GDO DC 3/4HP",
"20": "Chamberlain MyQ Home Bridge",
"21": "LiftMaster MyQ Home Bridge",
"23": "Chamberlain Smart Garage Hub",
"24": "LiftMaster Smart Garage Hub",
"27": "LiftMaster Wi-Fi Wall Mount opener",
"28": "LiftMaster Commercial Wi-Fi Wall Mount operator",
"80": "EU LiftMaster Ethernet Gateway",
"81": "EU Chamberlain Ethernet Gateway",
}

View File

@ -27,6 +27,8 @@ from homeassistant.helpers.event import async_call_later
from .const import (
DOMAIN,
KNOWN_MODELS,
MANUFACTURER,
MYQ_COORDINATOR,
MYQ_DEVICE_STATE,
MYQ_DEVICE_STATE_ONLINE,
@ -181,9 +183,12 @@ class MyQDevice(CoverDevice):
device_info = {
"identifiers": {(DOMAIN, self._device.device_id)},
"name": self._device.name,
"manufacturer": "The Chamberlain Group Inc.",
"manufacturer": MANUFACTURER,
"sw_version": self._device.firmware_version,
}
model = KNOWN_MODELS.get(self._device.device_id[2:4])
if model:
device_info["model"] = model
if self._device.parent_device_id:
device_info["via_device"] = (DOMAIN, self._device.parent_device_id)
return device_info

View File

@ -0,0 +1,20 @@
"""The scene tests for the myq platform."""
from homeassistant.const import STATE_ON
from .util import async_init_integration
async def test_create_binary_sensors(hass):
"""Test creation of binary_sensors."""
await async_init_integration(hass)
state = hass.states.get("binary_sensor.happy_place_myq_gateway")
assert state.state == STATE_ON
expected_attributes = {"device_class": "connectivity"}
# 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
)

View File

@ -0,0 +1,50 @@
"""The scene tests for the myq platform."""
from homeassistant.const import STATE_CLOSED
from .util import async_init_integration
async def test_create_covers(hass):
"""Test creation of covers."""
await async_init_integration(hass)
state = hass.states.get("cover.large_garage_door")
assert state.state == STATE_CLOSED
expected_attributes = {
"device_class": "garage",
"friendly_name": "Large Garage Door",
"supported_features": 3,
}
# 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("cover.small_garage_door")
assert state.state == STATE_CLOSED
expected_attributes = {
"device_class": "garage",
"friendly_name": "Small Garage Door",
"supported_features": 3,
}
# 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("cover.gate")
assert state.state == STATE_CLOSED
expected_attributes = {
"device_class": "gate",
"friendly_name": "Gate",
"supported_features": 3,
}
# 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
)

View File

@ -0,0 +1,42 @@
"""Tests for the myq integration."""
import json
from asynctest import patch
from homeassistant.components.myq.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 myq integration in Home Assistant."""
devices_fixture = "myq/devices.json"
devices_json = load_fixture(devices_fixture)
devices_dict = json.loads(devices_json)
def _handle_mock_api_request(method, endpoint, **kwargs):
if endpoint == "Login":
return {"SecurityToken": 1234}
elif endpoint == "My":
return {"Account": {"Id": 1}}
elif endpoint == "Accounts/1/Devices":
return devices_dict
return {}
with patch("pymyq.api.API.request", side_effect=_handle_mock_api_request):
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

133
tests/fixtures/myq/devices.json vendored Normal file
View File

@ -0,0 +1,133 @@
{
"count" : 4,
"href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices",
"items" : [
{
"device_type" : "ethernetgateway",
"created_date" : "2020-02-10T22:54:58.423",
"href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial",
"device_family" : "gateway",
"name" : "Happy place",
"device_platform" : "myq",
"state" : {
"homekit_enabled" : false,
"pending_bootload_abandoned" : false,
"online" : true,
"last_status" : "2020-03-30T02:49:46.4121303Z",
"physical_devices" : [],
"firmware_version" : "1.6",
"learn_mode" : false,
"learn" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial/learn",
"homekit_capable" : false,
"updated_date" : "2020-03-30T02:49:46.4171299Z"
},
"serial_number" : "gateway_serial"
},
{
"serial_number" : "gate_serial",
"state" : {
"report_ajar" : false,
"aux_relay_delay" : "00:00:00",
"is_unattended_close_allowed" : true,
"door_ajar_interval" : "00:00:00",
"aux_relay_behavior" : "None",
"last_status" : "2020-03-30T02:47:40.2794038Z",
"online" : true,
"rex_fires_door" : false,
"close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/close",
"invalid_shutout_period" : "00:00:00",
"invalid_credential_window" : "00:00:00",
"use_aux_relay" : false,
"command_channel_report_status" : false,
"last_update" : "2020-03-28T23:07:39.5611776Z",
"door_state" : "closed",
"max_invalid_attempts" : 0,
"open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/open",
"passthrough_interval" : "00:00:00",
"control_from_browser" : false,
"report_forced" : false,
"is_unattended_open_allowed" : true
},
"parent_device_id" : "gateway_serial",
"name" : "Gate",
"device_platform" : "myq",
"device_family" : "garagedoor",
"parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial",
"href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial",
"device_type" : "gate",
"created_date" : "2020-02-10T22:54:58.423"
},
{
"parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial",
"href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial",
"device_type" : "wifigaragedooropener",
"created_date" : "2020-02-10T22:55:25.863",
"device_platform" : "myq",
"name" : "Large Garage Door",
"device_family" : "garagedoor",
"serial_number" : "large_garage_serial",
"state" : {
"report_forced" : false,
"is_unattended_open_allowed" : true,
"passthrough_interval" : "00:00:00",
"control_from_browser" : false,
"attached_work_light_error_present" : false,
"max_invalid_attempts" : 0,
"open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/open",
"command_channel_report_status" : false,
"last_update" : "2020-03-28T23:58:55.5906643Z",
"door_state" : "closed",
"invalid_shutout_period" : "00:00:00",
"use_aux_relay" : false,
"invalid_credential_window" : "00:00:00",
"rex_fires_door" : false,
"close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/close",
"online" : true,
"last_status" : "2020-03-30T02:49:46.4121303Z",
"aux_relay_behavior" : "None",
"door_ajar_interval" : "00:00:00",
"gdo_lock_connected" : false,
"report_ajar" : false,
"aux_relay_delay" : "00:00:00",
"is_unattended_close_allowed" : true
},
"parent_device_id" : "gateway_serial"
},
{
"serial_number" : "small_garage_serial",
"state" : {
"last_status" : "2020-03-30T02:48:45.7501595Z",
"online" : true,
"report_ajar" : false,
"aux_relay_delay" : "00:00:00",
"is_unattended_close_allowed" : true,
"gdo_lock_connected" : false,
"door_ajar_interval" : "00:00:00",
"aux_relay_behavior" : "None",
"attached_work_light_error_present" : false,
"control_from_browser" : false,
"passthrough_interval" : "00:00:00",
"is_unattended_open_allowed" : true,
"report_forced" : false,
"close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/close",
"rex_fires_door" : false,
"invalid_credential_window" : "00:00:00",
"use_aux_relay" : false,
"invalid_shutout_period" : "00:00:00",
"door_state" : "closed",
"last_update" : "2020-03-26T15:45:31.4713796Z",
"command_channel_report_status" : false,
"open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/open",
"max_invalid_attempts" : 0
},
"parent_device_id" : "gateway_serial",
"device_platform" : "myq",
"name" : "Small Garage Door",
"device_family" : "garagedoor",
"parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial",
"href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial",
"device_type" : "wifigaragedooropener",
"created_date" : "2020-02-10T23:11:47.487"
}
]
}