361 files
This commit is contained in:
@@ -7,10 +7,13 @@ from typing import TYPE_CHECKING
|
||||
from homeassistant.const import CONF_ENTITY_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.helper_integration import async_remove_helper_devices
|
||||
from homeassistant.helpers.helper_integration import (
|
||||
async_handle_source_entity_changes,
|
||||
async_remove_helper_devices,
|
||||
)
|
||||
|
||||
from .config_flow import SpookInverseConfigFlowHandler
|
||||
from .const import CONF_HIDE_SOURCE
|
||||
from .const import CONF_HIDE_SOURCE, DOMAIN
|
||||
|
||||
MIGRATION_MINOR_VERSION = SpookInverseConfigFlowHandler.MINOR_VERSION
|
||||
|
||||
@@ -36,6 +39,40 @@ def async_get_source_entity_device_id(
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up from a config entry."""
|
||||
source = entry.options[CONF_ENTITY_ID]
|
||||
|
||||
# Remembered for when the options change, which is the only moment the
|
||||
# previous source, and whether this inverse hid it, is still known.
|
||||
# Somebody pointing the inverse elsewhere would otherwise leave the old
|
||||
# one hidden for good.
|
||||
entry.runtime_data = (source, entry.options[CONF_HIDE_SOURCE])
|
||||
|
||||
@callback
|
||||
def _follow_the_source(source_entity_id: str) -> None:
|
||||
"""Keep up with the source being renamed, the way core helpers do.
|
||||
|
||||
The inverse listens to its source by entity ID, and a rename gives it
|
||||
a new one: without this it keeps listening to a name nobody uses and
|
||||
switching something that is not there.
|
||||
"""
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, options={**entry.options, CONF_ENTITY_ID: source_entity_id}
|
||||
)
|
||||
|
||||
# A source stored as a registry ID that is gone resolves to nothing, and
|
||||
# Home Assistant raises on following that. The inverse still sets up, and
|
||||
# is unavailable, which is the truth about it.
|
||||
if er.async_resolve_entity_id(er.async_get(hass), source):
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=_follow_the_source,
|
||||
source_device_id=async_get_source_entity_device_id(hass, source),
|
||||
source_entity_id_or_uuid=source,
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry,
|
||||
(entry.options["inverse_type"],),
|
||||
@@ -45,10 +82,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
"""Update listener, called when the config entry options are changed.
|
||||
|
||||
Hiding happens in the options flow. Showing the source again happens
|
||||
here, because only here are both the old options and the new ones known,
|
||||
and showing it again is only this inverse's to do if it was the one that
|
||||
hid it.
|
||||
"""
|
||||
previous_source, previously_hidden = entry.runtime_data
|
||||
if previously_hidden and (
|
||||
previous_source != entry.options[CONF_ENTITY_ID]
|
||||
or not entry.options[CONF_HIDE_SOURCE]
|
||||
):
|
||||
async_release_source(hass, previous_source)
|
||||
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_release_source(hass: HomeAssistant, source: str) -> None:
|
||||
"""Show a source again that this inverse hid and no longer hides.
|
||||
|
||||
Only if Spook hid it, and only if no other inverse still wants it hidden:
|
||||
two inverses of one source share the hiding, and one of them letting go
|
||||
is not the other one letting go. After a rename the old name is in the
|
||||
registry no more, and the entry that is, with its hiding, is left alone.
|
||||
"""
|
||||
registry = er.async_get(hass)
|
||||
if not (entity_id := er.async_resolve_entity_id(registry, source)):
|
||||
return
|
||||
if (entity_entry := registry.async_get(entity_id)) is None:
|
||||
return
|
||||
if entity_entry.hidden_by != er.RegistryEntryHider.INTEGRATION:
|
||||
return
|
||||
|
||||
# Every inverse as it is now, this one with its new options. A removed
|
||||
# one is already gone from the list by the time it is asked about.
|
||||
for other in hass.config_entries.async_entries(DOMAIN):
|
||||
if other.options.get(CONF_HIDE_SOURCE) and (
|
||||
er.async_resolve_entity_id(registry, other.options[CONF_ENTITY_ID])
|
||||
== entity_id
|
||||
):
|
||||
return
|
||||
|
||||
registry.async_update_entity(entity_id, hidden_by=None)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(
|
||||
@@ -59,19 +138,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry, unhide the source entity."""
|
||||
registry = er.async_get(hass)
|
||||
if not entry.options[CONF_HIDE_SOURCE]:
|
||||
return
|
||||
if not (
|
||||
entity_id := er.async_resolve_entity_id(registry, entry.options[CONF_ENTITY_ID])
|
||||
):
|
||||
return
|
||||
if (entity_entry := registry.async_get(entity_id)) is None:
|
||||
return
|
||||
if entity_entry.hidden_by != er.RegistryEntryHider.INTEGRATION:
|
||||
return
|
||||
|
||||
registry.async_update_entity(entity_id, hidden_by=None)
|
||||
if entry.options[CONF_HIDE_SOURCE]:
|
||||
async_release_source(hass, entry.options[CONF_ENTITY_ID])
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -5,9 +5,8 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.const import CONF_ENTITY_ID, STATE_ON, STATE_UNKNOWN
|
||||
from homeassistant.const import STATE_ON, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .entity import InverseEntity
|
||||
|
||||
@@ -22,10 +21,8 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize inverse config entry."""
|
||||
er.async_validate_entity_id(
|
||||
er.async_get(hass),
|
||||
config_entry.options[CONF_ENTITY_ID],
|
||||
)
|
||||
# The source is resolved by the entity itself, which stays unavailable
|
||||
# rather than failing to set up when the source is not there.
|
||||
async_add_entities([InverseBinarySensor(hass, config_entry)])
|
||||
|
||||
|
||||
|
||||
@@ -19,39 +19,61 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
entity_selector_without_own_entities,
|
||||
)
|
||||
|
||||
from .const import CONF_HIDE_SOURCE, DOMAIN, PLATFORMS
|
||||
from .const import (
|
||||
CONF_HIDE_SOURCE,
|
||||
CONF_INVERSE_POSITION,
|
||||
CONF_INVERSE_TILT,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
SOURCE_DOMAINS,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Coroutine, Mapping
|
||||
|
||||
|
||||
# A cover moves two ways, and a device can have either of them backwards
|
||||
# without the other: blinds that close when told to open, or slats that tilt
|
||||
# the wrong way on blinds that are otherwise fine.
|
||||
COVER_SCHEMA = {
|
||||
vol.Required(CONF_INVERSE_POSITION, default=True): selector.BooleanSelector(),
|
||||
vol.Required(CONF_INVERSE_TILT, default=False): selector.BooleanSelector(),
|
||||
}
|
||||
|
||||
|
||||
async def options_schema(
|
||||
domain: str | list[str],
|
||||
domain: str,
|
||||
handler: SchemaCommonFlowHandler,
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
return vol.Schema(
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): entity_selector_without_own_entities(
|
||||
cast(SchemaOptionsFlowHandler, handler.parent_handler),
|
||||
selector.EntitySelectorConfig(domain=domain),
|
||||
selector.EntitySelectorConfig(domain=SOURCE_DOMAINS[domain]),
|
||||
),
|
||||
vol.Required(CONF_HIDE_SOURCE, default=False): selector.BooleanSelector(),
|
||||
},
|
||||
)
|
||||
if domain == Platform.COVER:
|
||||
schema = schema.extend(COVER_SCHEMA)
|
||||
return schema
|
||||
|
||||
|
||||
def config_schema(domain: str | list[str]) -> vol.Schema:
|
||||
def config_schema(domain: str) -> vol.Schema:
|
||||
"""Generate config schema."""
|
||||
return vol.Schema(
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): selector.TextSelector(),
|
||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=domain),
|
||||
selector.EntitySelectorConfig(domain=SOURCE_DOMAINS[domain]),
|
||||
),
|
||||
vol.Required(CONF_HIDE_SOURCE, default=False): selector.BooleanSelector(),
|
||||
},
|
||||
)
|
||||
if domain == Platform.COVER:
|
||||
schema = schema.extend(COVER_SCHEMA)
|
||||
return schema
|
||||
|
||||
|
||||
async def choose_options_step(options: dict[str, Any]) -> str:
|
||||
@@ -83,10 +105,18 @@ CONFIG_FLOW = {
|
||||
config_schema(Platform.BINARY_SENSOR),
|
||||
validate_user_input=set_inverse_type(Platform.BINARY_SENSOR),
|
||||
),
|
||||
Platform.COVER: SchemaFlowFormStep(
|
||||
config_schema(Platform.COVER),
|
||||
validate_user_input=set_inverse_type(Platform.COVER),
|
||||
),
|
||||
Platform.SWITCH: SchemaFlowFormStep(
|
||||
config_schema(Platform.SWITCH),
|
||||
validate_user_input=set_inverse_type(Platform.SWITCH),
|
||||
),
|
||||
Platform.VALVE: SchemaFlowFormStep(
|
||||
config_schema(Platform.VALVE),
|
||||
validate_user_input=set_inverse_type(Platform.VALVE),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +125,9 @@ OPTIONS_FLOW = {
|
||||
Platform.BINARY_SENSOR: SchemaFlowFormStep(
|
||||
partial(options_schema, Platform.BINARY_SENSOR),
|
||||
),
|
||||
Platform.COVER: SchemaFlowFormStep(partial(options_schema, Platform.COVER)),
|
||||
Platform.SWITCH: SchemaFlowFormStep(partial(options_schema, Platform.SWITCH)),
|
||||
Platform.VALVE: SchemaFlowFormStep(partial(options_schema, Platform.VALVE)),
|
||||
}
|
||||
|
||||
|
||||
@@ -129,22 +161,32 @@ class SpookInverseConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
hass: HomeAssistant,
|
||||
options: Mapping[str, Any],
|
||||
) -> None:
|
||||
"""Hide or unhide the source entity as requested."""
|
||||
hidden_by = (
|
||||
er.RegistryEntryHider.INTEGRATION if options[CONF_HIDE_SOURCE] else None
|
||||
)
|
||||
_async_hide_source(hass, options[CONF_ENTITY_ID], hidden_by)
|
||||
"""Hide the source entity if requested.
|
||||
|
||||
Showing it again is left to the update listener, which knows what the
|
||||
options were before, and whether this inverse was the one hiding it.
|
||||
"""
|
||||
if options[CONF_HIDE_SOURCE]:
|
||||
_async_hide_source(
|
||||
hass, options[CONF_ENTITY_ID], er.RegistryEntryHider.INTEGRATION
|
||||
)
|
||||
|
||||
|
||||
def _async_hide_source(
|
||||
hass: HomeAssistant,
|
||||
source_entity_id: str,
|
||||
hidden_by: er.RegistryEntryHider | None,
|
||||
hidden_by: er.RegistryEntryHider,
|
||||
) -> None:
|
||||
"""Hide or unhide inverse source."""
|
||||
"""Hide inverse source.
|
||||
|
||||
Never over somebody's own decision: a source they hid themselves in its
|
||||
entity settings stays theirs.
|
||||
"""
|
||||
registry = er.async_get(hass)
|
||||
if not (entity_id := er.async_resolve_entity_id(registry, source_entity_id)):
|
||||
return
|
||||
if entity_id not in registry.entities:
|
||||
if (entity_entry := registry.async_get(entity_id)) is None:
|
||||
return
|
||||
if entity_entry.hidden_by == er.RegistryEntryHider.USER:
|
||||
return
|
||||
registry.async_update_entity(entity_id, hidden_by=hidden_by)
|
||||
|
||||
@@ -5,9 +5,20 @@ from homeassistant.const import Platform
|
||||
DOMAIN = "spook_inverse"
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.COVER,
|
||||
Platform.SWITCH,
|
||||
Platform.VALVE,
|
||||
]
|
||||
|
||||
# What each kind of inverse can be made of. An on/off helper or a light
|
||||
# inverts as well as a switch does, and reads as well as a binary sensor.
|
||||
SOURCE_DOMAINS: dict[str, list[str]] = {
|
||||
Platform.BINARY_SENSOR: [Platform.BINARY_SENSOR, "input_boolean", Platform.LIGHT],
|
||||
Platform.COVER: [Platform.COVER],
|
||||
Platform.SWITCH: [Platform.SWITCH, "input_boolean", Platform.LIGHT],
|
||||
Platform.VALVE: [Platform.VALVE],
|
||||
}
|
||||
|
||||
CONF_HIDE_SOURCE = "hide_source"
|
||||
CONF_INVERSE_POSTITION = "inverse_position"
|
||||
CONF_INVERSE_POSITION = "inverse_position"
|
||||
CONF_INVERSE_TILT = "inverse_tilt"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from enum import IntFlag
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -13,7 +14,7 @@ from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import (
|
||||
@@ -26,6 +27,24 @@ if TYPE_CHECKING:
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
|
||||
def swapped_features[FeatureT: IntFlag](
|
||||
features: FeatureT,
|
||||
one: FeatureT,
|
||||
other: FeatureT,
|
||||
) -> FeatureT:
|
||||
"""Return the features with these two traded places.
|
||||
|
||||
Something that can only be opened is, upside down, something that can
|
||||
only be closed.
|
||||
"""
|
||||
swapped = features & ~(one | other)
|
||||
if features & one:
|
||||
swapped |= other
|
||||
if features & other:
|
||||
swapped |= one
|
||||
return swapped
|
||||
|
||||
|
||||
class InverseEntity(Entity): # pylint: disable=too-many-instance-attributes
|
||||
"""Inverse entity."""
|
||||
|
||||
@@ -39,7 +58,13 @@ class InverseEntity(Entity): # pylint: disable=too-many-instance-attributes
|
||||
) -> None:
|
||||
"""Initialize an inverse entity."""
|
||||
super().__init__()
|
||||
self._entity_id = config_entry.options[CONF_ENTITY_ID]
|
||||
# The source can be stored as its entity registry ID, which the state
|
||||
# machine knows nothing about. Following that would leave the inverse
|
||||
# unavailable next to a source that is perfectly fine.
|
||||
source = config_entry.options[CONF_ENTITY_ID]
|
||||
self._entity_id = (
|
||||
er.async_resolve_entity_id(er.async_get(hass), source) or source
|
||||
)
|
||||
self._attr_name = config_entry.title
|
||||
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: self._entity_id}
|
||||
self._attr_unique_id = config_entry.entry_id
|
||||
@@ -87,14 +112,27 @@ class InverseEntity(Entity): # pylint: disable=too-many-instance-attributes
|
||||
if (
|
||||
state := self.hass.states.get(self._entity_id)
|
||||
) is None or state.state == STATE_UNAVAILABLE:
|
||||
# Written down as well, or the inverse goes on showing what it was
|
||||
# before its source went, as if nothing happened.
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_available = True
|
||||
self._attr_supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
self._attr_icon = state.attributes.get(ATTR_ICON)
|
||||
|
||||
# Only from a source of its own kind. A light's features are not a
|
||||
# switch's, and a device class means something else for every domain:
|
||||
# taken across, they claim things this entity cannot do or be.
|
||||
if split_entity_id(self._entity_id)[0] == split_entity_id(self.entity_id)[0]:
|
||||
self._attr_supported_features = state.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES
|
||||
)
|
||||
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
else:
|
||||
self._attr_supported_features = None
|
||||
self._attr_device_class = None
|
||||
|
||||
self.async_update_state(state)
|
||||
|
||||
state_attributes = {
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "calculated",
|
||||
"issue_tracker": "https://github.com/frenck/spook/issues",
|
||||
"requirements": [],
|
||||
"version": "5.7.0"
|
||||
"version": "5.8.1"
|
||||
}
|
||||
|
||||
@@ -4,18 +4,16 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.switch import DOMAIN, SwitchEntity
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ENTITY_ID,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
|
||||
|
||||
from .entity import InverseEntity
|
||||
|
||||
@@ -30,15 +28,17 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize inverse config entry."""
|
||||
er.async_validate_entity_id(
|
||||
er.async_get(hass),
|
||||
config_entry.options[CONF_ENTITY_ID],
|
||||
)
|
||||
# The source is resolved by the entity itself, which stays unavailable
|
||||
# rather than failing to set up when the source is not there.
|
||||
async_add_entities([InverseSwitch(hass, config_entry)])
|
||||
|
||||
|
||||
class InverseSwitch(InverseEntity, SwitchEntity):
|
||||
"""Inverse switch."""
|
||||
"""Inverse switch.
|
||||
|
||||
Its source can be an on/off helper or a light as well, so it is told what
|
||||
to do in its own terms: a light is turned off with the light actions.
|
||||
"""
|
||||
|
||||
@callback
|
||||
def async_update_state(self, state: State) -> None:
|
||||
@@ -51,7 +51,7 @@ class InverseSwitch(InverseEntity, SwitchEntity):
|
||||
async def async_turn_on(self, **_: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN,
|
||||
split_entity_id(self._entity_id)[0],
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: self._entity_id},
|
||||
blocking=True,
|
||||
@@ -61,7 +61,7 @@ class InverseSwitch(InverseEntity, SwitchEntity):
|
||||
async def async_turn_off(self, **_: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN,
|
||||
split_entity_id(self._entity_id)[0],
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: self._entity_id},
|
||||
blocking=True,
|
||||
@@ -71,7 +71,7 @@ class InverseSwitch(InverseEntity, SwitchEntity):
|
||||
async def async_toggle(self, **_: Any) -> None:
|
||||
"""Toggle the entity."""
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN,
|
||||
split_entity_id(self._entity_id)[0],
|
||||
SERVICE_TOGGLE,
|
||||
{ATTR_ENTITY_ID: self._entity_id},
|
||||
blocking=True,
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
},
|
||||
"cover": {
|
||||
"data": {
|
||||
"entity_id": "Source entity",
|
||||
"hide_source": "Hide source entity",
|
||||
"inverse_position": "Inverse opening, closing and position",
|
||||
"inverse_tilt": "Inverse tilt",
|
||||
"name": "Name"
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
},
|
||||
"switch": {
|
||||
"data": {
|
||||
"entity_id": "Source entity",
|
||||
@@ -17,11 +27,21 @@
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
},
|
||||
"valve": {
|
||||
"data": {
|
||||
"entity_id": "Source entity",
|
||||
"hide_source": "Hide source entity",
|
||||
"name": "Name"
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
},
|
||||
"user": {
|
||||
"description": "This helper allow you to inverse the behavior of an entity. For example, reverse the open/close or on/off of binary sensors, and switches.",
|
||||
"description": "This helper allows you to invert the behavior of an entity. For example, reverse the open/close or on/off of binary sensors, covers, switches, and valves.",
|
||||
"menu_options": {
|
||||
"binary_sensor": "Inverse a binary sensor",
|
||||
"switch": "Inverse a switch"
|
||||
"cover": "Inverse a cover",
|
||||
"switch": "Inverse a switch",
|
||||
"valve": "Inverse a valve"
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
}
|
||||
@@ -37,6 +57,16 @@
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
},
|
||||
"cover": {
|
||||
"data": {
|
||||
"entity_id": "Source entity",
|
||||
"hide_source": "Hide source entity",
|
||||
"inverse_position": "Inverse opening, closing and position",
|
||||
"inverse_tilt": "Inverse tilt",
|
||||
"name": "Name"
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
},
|
||||
"switch": {
|
||||
"data": {
|
||||
"entity_id": "Source entity",
|
||||
@@ -44,6 +74,14 @@
|
||||
"name": "Name"
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
},
|
||||
"valve": {
|
||||
"data": {
|
||||
"entity_id": "Source entity",
|
||||
"hide_source": "Hide source entity",
|
||||
"name": "Name"
|
||||
},
|
||||
"title": "Inverse 👻"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Овај помоћник ти омогућава да преокренеш понашање ентитета. На пример, да обрнеш стања отворено/затворено или укључено/искључено за бинарне сензоре и прекидаче.",
|
||||
"description": "Овај помоћник вам омогућава да обрнете понашање ентитета. На пример, преокрените стање отворено/затворено или укључено/искључено за бинарне сензоре, ролетне, прекидаче и вентиле.",
|
||||
"menu_options": {
|
||||
"binary_sensor": "Инвертуј бинарни сензор",
|
||||
"switch": "Инвертуј прекидач"
|
||||
"switch": "Инвертуј прекидач",
|
||||
"cover": "Инвертуј застор",
|
||||
"valve": "Инвертуј вентил"
|
||||
},
|
||||
"title": "Инвертуј 👻"
|
||||
},
|
||||
@@ -24,6 +26,24 @@
|
||||
"name": "Назив"
|
||||
},
|
||||
"title": "Инвертуј 👻"
|
||||
},
|
||||
"cover": {
|
||||
"data": {
|
||||
"entity_id": "Изворни ентитет",
|
||||
"hide_source": "Сакриј изворни ентитет",
|
||||
"inverse_position": "Инверуј отварање, затварање и позицију",
|
||||
"inverse_tilt": "Инверуј нагиб",
|
||||
"name": "Назив"
|
||||
},
|
||||
"title": "Инверуј 👻"
|
||||
},
|
||||
"valve": {
|
||||
"data": {
|
||||
"entity_id": "Изворни ентитет",
|
||||
"hide_source": "Сакриј изворни ентитет",
|
||||
"name": "Назив"
|
||||
},
|
||||
"title": "Инверуј 👻"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -44,8 +64,26 @@
|
||||
"hide_source": "Сакриј изворни ентитет",
|
||||
"name": "Назив"
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"data": {
|
||||
"entity_id": "Изворни ентитет",
|
||||
"hide_source": "Сакриј изворни ентитет",
|
||||
"inverse_position": "Инвертуј отварање, затварање и позицију",
|
||||
"inverse_tilt": "Инвертуј нагиб",
|
||||
"name": "Назив"
|
||||
},
|
||||
"title": "Инветуј 👻"
|
||||
},
|
||||
"valve": {
|
||||
"data": {
|
||||
"entity_id": "Изворни ентитет",
|
||||
"hide_source": "Сакриј изворни ентитет",
|
||||
"name": "Назив"
|
||||
},
|
||||
"title": "Инвертуј 👻"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Инвертуј 👻"
|
||||
"title": "Инвертуј👻"
|
||||
}
|
||||
|
||||
@@ -18,10 +18,30 @@
|
||||
"title": "Invertuj 👻"
|
||||
},
|
||||
"user": {
|
||||
"description": "Ovaj pomoćnik ti omogućava da preokreneš ponašanje entiteta. Na primer, da obrneš stanja otvoreno/zatvoreno ili uključeno/isključeno za binarne senzore i prekidače.",
|
||||
"description": "Ovaj pomoćnik vam omogućava da obrnete ponašanje entiteta. Na primer, preokrenite stanje otvoreno/zatvoreno ili uključeno/isključeno za binarne senzore, roletne, prekidače i ventile.",
|
||||
"menu_options": {
|
||||
"binary_sensor": "Invertuj binarni senzor",
|
||||
"switch": "Invertuj prekidač"
|
||||
"switch": "Invertuj prekidač",
|
||||
"cover": "Invertuj zastor",
|
||||
"valve": "Invertuj ventil"
|
||||
},
|
||||
"title": "Invertuj 👻"
|
||||
},
|
||||
"cover": {
|
||||
"data": {
|
||||
"entity_id": "Izvorni entitet",
|
||||
"hide_source": "Sakrij izvorni entitet",
|
||||
"inverse_position": "Invertuj otvaranje, zatvaranje i poziciju",
|
||||
"inverse_tilt": "Invertuj nagib",
|
||||
"name": "Naziv"
|
||||
},
|
||||
"title": "Invertuj 👻"
|
||||
},
|
||||
"valve": {
|
||||
"data": {
|
||||
"entity_id": "Izvorni entitet",
|
||||
"hide_source": "Sakrij izvorni entitet",
|
||||
"name": "Naziv"
|
||||
},
|
||||
"title": "Invertuj 👻"
|
||||
}
|
||||
@@ -44,8 +64,26 @@
|
||||
"hide_source": "Sakrij izvorni entitet"
|
||||
},
|
||||
"title": "Invertuj 👻"
|
||||
},
|
||||
"cover": {
|
||||
"data": {
|
||||
"hide_source": "Sakrij izvorni entitet",
|
||||
"entity_id": "Izvorni entitet",
|
||||
"inverse_position": "Invertuj otvaranje, zatvaranje i poziciju",
|
||||
"inverse_tilt": "Inverutuj nagib",
|
||||
"name": "Naziv"
|
||||
},
|
||||
"title": "Invertuj 👻"
|
||||
},
|
||||
"valve": {
|
||||
"data": {
|
||||
"hide_source": "Sakrij izvorni entitet",
|
||||
"entity_id": "Izvorni entitet",
|
||||
"name": "Naziv"
|
||||
},
|
||||
"title": "Invertuj 👻"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Invertuj 👻"
|
||||
"title": "Invertuj👻"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user