diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py
index 0ac4ea8cb59..2903eb926e5 100644
--- a/homeassistant/components/kmtronic/__init__.py
+++ b/homeassistant/components/kmtronic/__init__.py
@@ -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
diff --git a/homeassistant/components/kmtronic/config_flow.py b/homeassistant/components/kmtronic/config_flow.py
index 841c541dd4e..736e6a46c11 100644
--- a/homeassistant/components/kmtronic/config_flow.py
+++ b/homeassistant/components/kmtronic/config_flow.py
@@ -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,
+ }
+ ),
+ )
diff --git a/homeassistant/components/kmtronic/const.py b/homeassistant/components/kmtronic/const.py
index 8ca37a0b797..8b34d423724 100644
--- a/homeassistant/components/kmtronic/const.py
+++ b/homeassistant/components/kmtronic/const.py
@@ -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"
diff --git a/homeassistant/components/kmtronic/strings.json b/homeassistant/components/kmtronic/strings.json
index 7becb830d91..2aaa0d2f8dd 100644
--- a/homeassistant/components/kmtronic/strings.json
+++ b/homeassistant/components/kmtronic/strings.json
@@ -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)"
+ }
+ }
+ }
}
}
diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py
index 5970ec20cb8..d37cd54ce1a 100644
--- a/homeassistant/components/kmtronic/switch.py
+++ b/homeassistant/components/kmtronic/switch.py
@@ -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()
diff --git a/homeassistant/components/kmtronic/translations/en.json b/homeassistant/components/kmtronic/translations/en.json
index f15fe84c3ed..0a1bde9fb19 100644
--- a/homeassistant/components/kmtronic/translations/en.json
+++ b/homeassistant/components/kmtronic/translations/en.json
@@ -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)"
}
}
}
diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py
index ebbbf626451..b5ebdc79c8b 100644
--- a/tests/components/kmtronic/test_config_flow.py
+++ b/tests/components/kmtronic/test_config_flow.py
@@ -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="00",
+ )
+ 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="00",
- )
- 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
diff --git a/tests/components/kmtronic/test_init.py b/tests/components/kmtronic/test_init.py
new file mode 100644
index 00000000000..1b9cf7cb407
--- /dev/null
+++ b/tests/components/kmtronic/test_init.py
@@ -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="00",
+ )
+ 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
diff --git a/tests/components/kmtronic/test_switch.py b/tests/components/kmtronic/test_switch.py
index 5eec3537176..df8ecda2c2e 100644
--- a/tests/components/kmtronic/test_switch.py
+++ b/tests/components/kmtronic/test_switch.py
@@ -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="00",
+ )
+
+ 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"