Add option to reverse switch behaviour in KMTronic (#47532)
This commit is contained in:
parent
520c4a8ee3
commit
ee25723468
9 changed files with 240 additions and 71 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
62
tests/components/kmtronic/test_init.py
Normal file
62
tests/components/kmtronic/test_init.py
Normal 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
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue