Add option to reverse switch behaviour in KMTronic (#47532)

This commit is contained in:
Diogo Gomes 2021-03-08 21:56:24 +00:00 committed by GitHub
parent 520c4a8ee3
commit ee25723468
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 240 additions and 71 deletions

View file

@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DATA_COORDINATOR, DATA_HOST, DATA_HUB, DOMAIN, MANUFACTURER
from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
@ -67,7 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN][entry.entry_id] = {
DATA_HUB: hub,
DATA_HOST: entry.data[DATA_HOST],
DATA_COORDINATOR: coordinator,
}
@ -76,9 +75,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.config_entries.async_forward_entry_setup(entry, platform)
)
update_listener = entry.add_update_listener(async_update_options)
hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener
return True
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Update options."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
@ -90,6 +97,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
)
)
if unload_ok:
update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]
update_listener()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View file

@ -8,13 +8,21 @@ import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .const import CONF_REVERSE
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema({CONF_HOST: str, CONF_USERNAME: str, CONF_PASSWORD: str})
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
async def validate_input(hass: core.HomeAssistant, data):
@ -44,6 +52,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return KMTronicOptionsFlow(config_entry)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
@ -71,3 +85,28 @@ class CannotConnect(exceptions.HomeAssistantError):
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
class KMTronicOptionsFlow(config_entries.OptionsFlow):
"""Handle options."""
def __init__(self, config_entry):
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_REVERSE,
default=self.config_entry.options.get(CONF_REVERSE),
): bool,
}
),
)

View file

@ -2,10 +2,13 @@
DOMAIN = "kmtronic"
CONF_REVERSE = "reverse"
DATA_HUB = "hub"
DATA_HOST = "host"
DATA_COORDINATOR = "coordinator"
MANUFACTURER = "KMtronic"
ATTR_MANUFACTURER = "manufacturer"
ATTR_IDENTIFIERS = "identifiers"
UPDATE_LISTENER = "update_listener"

View file

@ -17,5 +17,14 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"step": {
"init": {
"data": {
"reverse": "Reverse switch logic (use NC)"
}
}
}
}
}

View file

@ -3,19 +3,19 @@
from homeassistant.components.switch import SwitchEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DATA_COORDINATOR, DATA_HOST, DATA_HUB, DOMAIN
from .const import CONF_REVERSE, DATA_COORDINATOR, DATA_HUB, DOMAIN
async def async_setup_entry(hass, entry, async_add_entities):
"""Config entry example."""
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
hub = hass.data[DOMAIN][entry.entry_id][DATA_HUB]
host = hass.data[DOMAIN][entry.entry_id][DATA_HOST]
reverse = entry.options.get(CONF_REVERSE, False)
await hub.async_get_relays()
async_add_entities(
[
KMtronicSwitch(coordinator, host, relay, entry.unique_id)
KMtronicSwitch(coordinator, relay, reverse, entry.entry_id)
for relay in hub.relays
]
)
@ -24,17 +24,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
class KMtronicSwitch(CoordinatorEntity, SwitchEntity):
"""KMtronic Switch Entity."""
def __init__(self, coordinator, host, relay, config_entry_id):
def __init__(self, coordinator, relay, reverse, config_entry_id):
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self._host = host
self._relay = relay
self._config_entry_id = config_entry_id
@property
def available(self) -> bool:
"""Return whether the entity is available."""
return self.coordinator.last_update_success
self._reverse = reverse
@property
def name(self) -> str:
@ -46,22 +41,25 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity):
"""Return the unique ID of the entity."""
return f"{self._config_entry_id}_relay{self._relay.id}"
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return True
@property
def is_on(self):
"""Return entity state."""
if self._reverse:
return not self._relay.is_on
return self._relay.is_on
async def async_turn_on(self, **kwargs) -> None:
"""Turn the switch on."""
await self._relay.turn_on()
if self._reverse:
await self._relay.turn_off()
else:
await self._relay.turn_on()
self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None:
"""Turn the switch off."""
await self._relay.turn_off()
if self._reverse:
await self._relay.turn_on()
else:
await self._relay.turn_off()
self.async_write_ha_state()

View file

@ -13,7 +13,17 @@
"data": {
"host": "Host",
"password": "Password",
"username": "Username"
"username": "Username",
"reverse": "Reverse switch logic (use NC)"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"reverse": "Reverse switch logic (use NC)"
}
}
}

View file

@ -3,9 +3,9 @@ from unittest.mock import Mock, patch
from aiohttp import ClientConnectorError, ClientResponseError
from homeassistant import config_entries, setup
from homeassistant.components.kmtronic.const import DOMAIN
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.kmtronic.const import CONF_REVERSE, DOMAIN
from homeassistant.config_entries import ENTRY_STATE_LOADED
from tests.common import MockConfigEntry
@ -49,6 +49,43 @@ async def test_form(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_options(hass, aioclient_mock):
"""Test that the options form."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
"host": "1.1.1.1",
"username": "admin",
"password": "admin",
},
)
config_entry.add_to_hass(hass)
aioclient_mock.get(
"http://1.1.1.1/status.xml",
text="<response><relay0>0</relay0><relay1>0</relay1></response>",
)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_LOADED
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_REVERSE: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {CONF_REVERSE: True}
await hass.async_block_till_done()
assert config_entry.state == "loaded"
async def test_form_invalid_auth(hass):
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
@ -116,30 +153,3 @@ async def test_form_unknown_error(hass):
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}
async def test_unload_config_entry(hass, aioclient_mock):
"""Test entry unloading."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={"host": "1.1.1.1", "username": "admin", "password": "admin"},
)
config_entry.add_to_hass(hass)
aioclient_mock.get(
"http://1.1.1.1/status.xml",
text="<response><relay0>0</relay0><relay1>0</relay1></response>",
)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 1
assert config_entries[0] is config_entry
assert config_entry.state == ENTRY_STATE_LOADED
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_NOT_LOADED

View file

@ -0,0 +1,62 @@
"""The tests for the KMtronic component."""
import asyncio
from homeassistant.components.kmtronic.const import DOMAIN
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_RETRY,
)
from tests.common import MockConfigEntry
async def test_unload_config_entry(hass, aioclient_mock):
"""Test entry unloading."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
"host": "1.1.1.1",
"username": "admin",
"password": "admin",
},
)
config_entry.add_to_hass(hass)
aioclient_mock.get(
"http://1.1.1.1/status.xml",
text="<response><relay0>0</relay0><relay1>0</relay1></response>",
)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_LOADED
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_NOT_LOADED
async def test_config_entry_not_ready(hass, aioclient_mock):
"""Tests configuration entry not ready."""
aioclient_mock.get(
"http://1.1.1.1/status.xml",
exc=asyncio.TimeoutError(),
)
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
"host": "1.1.1.1",
"username": "foo",
"password": "bar",
},
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_SETUP_RETRY

View file

@ -3,7 +3,6 @@ import asyncio
from datetime import timedelta
from homeassistant.components.kmtronic.const import DOMAIN
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@ -88,24 +87,6 @@ async def test_update(hass, aioclient_mock):
assert state.state == "on"
async def test_config_entry_not_ready(hass, aioclient_mock):
"""Tests configuration entry not ready."""
aioclient_mock.get(
"http://1.1.1.1/status.xml",
exc=asyncio.TimeoutError(),
)
config_entry = MockConfigEntry(
domain=DOMAIN, data={"host": "1.1.1.1", "username": "foo", "password": "bar"}
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
async def test_failed_update(hass, aioclient_mock):
"""Tests coordinator update fails."""
now = dt_util.utcnow()
@ -148,3 +129,51 @@ async def test_failed_update(hass, aioclient_mock):
await hass.async_block_till_done()
state = hass.states.get("switch.relay1")
assert state.state == STATE_UNAVAILABLE
async def test_relay_on_off_reversed(hass, aioclient_mock):
"""Tests the relay turns on correctly when configured as reverse."""
aioclient_mock.get(
"http://1.1.1.1/status.xml",
text="<response><relay0>0</relay0><relay1>0</relay1></response>",
)
MockConfigEntry(
domain=DOMAIN,
data={"host": "1.1.1.1", "username": "foo", "password": "bar"},
options={"reverse": True},
).add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
# Mocks the response for turning a relay1 off
aioclient_mock.get(
"http://1.1.1.1/FF0101",
text="",
)
state = hass.states.get("switch.relay1")
assert state.state == "on"
await hass.services.async_call(
"switch", "turn_off", {"entity_id": "switch.relay1"}, blocking=True
)
await hass.async_block_till_done()
state = hass.states.get("switch.relay1")
assert state.state == "off"
# Mocks the response for turning a relay1 off
aioclient_mock.get(
"http://1.1.1.1/FF0100",
text="",
)
await hass.services.async_call(
"switch", "turn_on", {"entity_id": "switch.relay1"}, blocking=True
)
await hass.async_block_till_done()
state = hass.states.get("switch.relay1")
assert state.state == "on"