Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add invert option to switch_as_x #107535

Merged
merged 10 commits into from
Jan 24, 2024
34 changes: 33 additions & 1 deletion homeassistant/components/switch_as_x/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.typing import EventType

from .const import CONF_TARGET_DOMAIN
from .const import CONF_INVERT, CONF_TARGET_DOMAIN
from .light import LightSwitch

__all__ = ["LightSwitch"]
Expand Down Expand Up @@ -91,6 +91,7 @@ async def async_registry_updated(
hass, entity_id, async_registry_updated
)
)
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))

device_id = async_add_to_device(hass, entry, entity_id)

Expand All @@ -100,6 +101,37 @@ async def async_registry_updated(
return True


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
)

if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
options = {**config_entry.options}
if config_entry.minor_version < 2:
options[CONF_INVERT] = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that if someone has upgraded and configured a switch_as_x with invert=True and then downgrades it will change the option for that entry to invert=False hence changing how that entity works (even if the user then upgrade again).

It should probably only set this option if it's not already there?

config_entry.version = 1
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
config_entry.minor_version = 2
hass.config_entries.async_update_entry(config_entry, options=options)

_LOGGER.debug(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)

return True


async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener, called when the config entry options are changed."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(
Expand Down
13 changes: 12 additions & 1 deletion homeassistant/components/switch_as_x/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
wrapped_entity_config_entry_title,
)

from .const import CONF_TARGET_DOMAIN, DOMAIN
from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN

TARGET_DOMAIN_OPTIONS = [
selector.SelectOptionDict(value=Platform.COVER, label="Cover"),
Expand All @@ -32,6 +32,7 @@
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=Platform.SWITCH),
),
vol.Required(CONF_INVERT): selector.BooleanSelector(),
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
vol.Required(CONF_TARGET_DOMAIN): selector.SelectSelector(
selector.SelectSelectorConfig(options=TARGET_DOMAIN_OPTIONS),
),
Expand All @@ -40,11 +41,21 @@
)
}

OPTIONS_FLOW = {
"init": SchemaFlowFormStep(
vol.Schema({vol.Required(CONF_INVERT): selector.BooleanSelector()})
),
}


class SwitchAsXConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for Switch as X."""

config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW

VERSION = 1
MINOR_VERSION = 2

def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title and hide the wrapped entity if registered."""
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/switch_as_x/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

DOMAIN: Final = "switch_as_x"

CONF_INVERT: Final = "invert"
CONF_TARGET_DOMAIN: Final = "target_domain"
15 changes: 10 additions & 5 deletions homeassistant/components/switch_as_x/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from homeassistant.helpers.event import EventStateChangedData
from homeassistant.helpers.typing import EventType

from .entity import BaseEntity
from .const import CONF_INVERT
from .entity import BaseInvertableEntity


async def async_setup_entry(
Expand All @@ -43,14 +44,15 @@ async def async_setup_entry(
hass,
config_entry.title,
COVER_DOMAIN,
config_entry.options[CONF_INVERT],
entity_id,
config_entry.entry_id,
)
]
)


class CoverSwitch(BaseEntity, CoverEntity):
class CoverSwitch(BaseInvertableEntity, CoverEntity):
"""Represents a Switch as a Cover."""

_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
Expand All @@ -59,7 +61,7 @@ async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
SERVICE_TURN_ON if not self._invert_state else SERVICE_TURN_OFF,
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
context=self._context,
Expand All @@ -69,7 +71,7 @@ async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_OFF if not self._invert_state else SERVICE_TURN_ON,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
context=self._context,
Expand All @@ -87,4 +89,7 @@ def async_state_changed_listener(
):
return

self._attr_is_closed = state.state != STATE_ON
if not self._invert_state:
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
self._attr_is_closed = state.state != STATE_ON
else:
self._attr_is_closed = state.state == STATE_ON
29 changes: 28 additions & 1 deletion homeassistant/components/switch_as_x/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def _async_state_changed_listener(
registry.async_update_entity_options(
self.entity_id,
SWITCH_AS_X_DOMAIN,
{"entity_id": self._switch_entity_id},
self.async_generate_entity_options(),
)

if not self._is_new_entity or not (
Expand Down Expand Up @@ -141,6 +141,11 @@ def copy_expose_settings() -> None:
copy_custom_name(wrapped_switch)
copy_expose_settings()

@callback
def async_generate_entity_options(self) -> dict[str, Any]:
"""Generate entity options."""
return {"entity_id": self._switch_entity_id, "invert": False}


class BaseToggleEntity(BaseEntity, ToggleEntity):
"""Represents a Switch as a ToggleEntity."""
Expand Down Expand Up @@ -178,3 +183,25 @@ def async_state_changed_listener(
return

self._attr_is_on = state.state == STATE_ON


class BaseInvertableEntity(BaseEntity):
"""Represents a Switch as an X."""

def __init__(
self,
hass: HomeAssistant,
config_entry_title: str,
domain: str,
invert: bool,
switch_entity_id: str,
unique_id: str,
) -> None:
"""Initialize Switch as an X."""
super().__init__(hass, config_entry_title, domain, switch_entity_id, unique_id)
self._invert_state = invert

@callback
def async_generate_entity_options(self) -> dict[str, Any]:
"""Generate entity options."""
return super().async_generate_entity_options() | {"invert": self._invert_state}
15 changes: 10 additions & 5 deletions homeassistant/components/switch_as_x/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
from homeassistant.helpers.event import EventStateChangedData
from homeassistant.helpers.typing import EventType

from .entity import BaseEntity
from .const import CONF_INVERT
from .entity import BaseInvertableEntity


async def async_setup_entry(
Expand All @@ -39,21 +40,22 @@ async def async_setup_entry(
hass,
config_entry.title,
LOCK_DOMAIN,
config_entry.options[CONF_INVERT],
entity_id,
config_entry.entry_id,
)
]
)


class LockSwitch(BaseEntity, LockEntity):
class LockSwitch(BaseInvertableEntity, LockEntity):
"""Represents a Switch as a Lock."""

async def async_lock(self, **kwargs: Any) -> None:
"""Lock the lock."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_OFF if not self._invert_state else SERVICE_TURN_ON,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
context=self._context,
Expand All @@ -63,7 +65,7 @@ async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the lock."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
SERVICE_TURN_ON if not self._invert_state else SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
context=self._context,
Expand All @@ -83,4 +85,7 @@ def async_state_changed_listener(

# Logic is the same as the lock device class for binary sensors
# on means open (unlocked), off means closed (locked)
self._attr_is_locked = state.state != STATE_ON
if not self._invert_state:
self._attr_is_locked = state.state != STATE_ON
else:
self._attr_is_locked = state.state == STATE_ON
16 changes: 16 additions & 0 deletions homeassistant/components/switch_as_x/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,23 @@
"description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden.",
"data": {
"entity_id": "Switch",
"invert": "Invert state",
"target_domain": "New Type"
},
"data_description": {
"invert": "Invert state, only supported for cover, lock and valve."
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"forecast": "[%key:component::switch_as_x::config::step::user::data::invert%]"
},
"data_description": {
"forecast": "[%key:component::switch_as_x::config::step::user::data_description::invert%]"
emontnemery marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
15 changes: 10 additions & 5 deletions homeassistant/components/switch_as_x/valve.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from homeassistant.helpers.event import EventStateChangedData
from homeassistant.helpers.typing import EventType

from .entity import BaseEntity
from .const import CONF_INVERT
from .entity import BaseInvertableEntity


async def async_setup_entry(
Expand All @@ -43,14 +44,15 @@ async def async_setup_entry(
hass,
config_entry.title,
VALVE_DOMAIN,
config_entry.options[CONF_INVERT],
entity_id,
config_entry.entry_id,
)
]
)


class ValveSwitch(BaseEntity, ValveEntity):
class ValveSwitch(BaseInvertableEntity, ValveEntity):
"""Represents a Switch as a Valve."""

_attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
Expand All @@ -60,7 +62,7 @@ async def async_open_valve(self, **kwargs: Any) -> None:
"""Open the valve."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
SERVICE_TURN_ON if not self._invert_state else SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
context=self._context,
Expand All @@ -70,7 +72,7 @@ async def async_close_valve(self, **kwargs: Any) -> None:
"""Close valve."""
await self.hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_OFF if not self._invert_state else SERVICE_TURN_ON,
{ATTR_ENTITY_ID: self._switch_entity_id},
blocking=True,
context=self._context,
Expand All @@ -88,4 +90,7 @@ def async_state_changed_listener(
):
return

self._attr_is_closed = state.state != STATE_ON
if not self._invert_state:
self._attr_is_closed = state.state != STATE_ON
else:
self._attr_is_closed = state.state == STATE_ON
38 changes: 38 additions & 0 deletions tests/components/switch_as_x/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
"""The tests for Switch as X platforms."""

from homeassistant.const import (
STATE_CLOSED,
STATE_LOCKED,
STATE_OFF,
STATE_ON,
STATE_OPEN,
STATE_UNLOCKED,
Platform,
)

PLATFORMS_TO_TEST = (
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.LOCK,
Platform.SIREN,
Platform.VALVE,
)

STATE_MAP = {
False: {
Platform.COVER: {STATE_ON: STATE_OPEN, STATE_OFF: STATE_CLOSED},
Platform.FAN: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF},
Platform.LIGHT: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF},
Platform.LOCK: {STATE_ON: STATE_UNLOCKED, STATE_OFF: STATE_LOCKED},
Platform.SIREN: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF},
Platform.VALVE: {STATE_ON: STATE_OPEN, STATE_OFF: STATE_CLOSED},
},
True: {
Platform.COVER: {STATE_ON: STATE_CLOSED, STATE_OFF: STATE_OPEN},
Platform.FAN: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF},
Platform.LIGHT: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF},
Platform.LOCK: {STATE_ON: STATE_LOCKED, STATE_OFF: STATE_UNLOCKED},
Platform.SIREN: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF},
Platform.VALVE: {STATE_ON: STATE_CLOSED, STATE_OFF: STATE_OPEN},
},
}
Loading
Loading