Skip to content

Commit

Permalink
feat: stop entity, add support for button and script (#4)
Browse files Browse the repository at this point in the history
* add optional stop entity

* add script and button to integration
  • Loading branch information
duhow authored Sep 6, 2024
1 parent 3be1273 commit 5d1c1ff
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 48 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Convert your (dummy) `switch` into a `cover`, and allow to control its position.

Additionally, if you interact with your physical switch, the position status will be updated as well.

**Optional:** If your cover uses a third button for stopping, you can also add it (normally your cover will stop once the up/down switch is turned off).

**Experimental:** You can add `scripts` to enable custom action (eg. MQTT calls), for easy integration with other hardware.

## Install

[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=duhow&repository=hass-cover-time-based&category=integration)
Expand Down
20 changes: 12 additions & 8 deletions custom_components/cover_time_based/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,27 @@
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowFormStep

from .const import CONF_ENTITY_DOWN
from .const import CONF_ENTITY_STOP
from .const import CONF_ENTITY_UP
from .const import CONF_TIME_CLOSE
from .const import CONF_TIME_OPEN
from .const import DOMAIN

DOMAIN_ENTITIES_ALLOWED = [Platform.SWITCH, Platform.LIGHT, Platform.BUTTON, "script"]

CONFIG_FLOW = {
"user": SchemaFlowFormStep(
vol.Schema(
{
vol.Required(CONF_NAME): selector.TextSelector(),
vol.Required(CONF_ENTITY_UP): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[Platform.SWITCH, Platform.LIGHT]
)
selector.EntitySelectorConfig(domain=DOMAIN_ENTITIES_ALLOWED)
),
vol.Required(CONF_ENTITY_DOWN): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[Platform.SWITCH, Platform.LIGHT]
)
selector.EntitySelectorConfig(domain=DOMAIN_ENTITIES_ALLOWED)
),
vol.Optional(CONF_ENTITY_STOP): selector.EntitySelector(
selector.EntitySelectorConfig(domain=DOMAIN_ENTITIES_ALLOWED)
),
vol.Required(CONF_TIME_OPEN, default=25): selector.NumberSelector(
selector.NumberSelectorConfig(
Expand Down Expand Up @@ -89,15 +91,17 @@ class CoverTimeBasedConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
options_flow = OPTIONS_FLOW

VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3

def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title and hide the wrapped entity if
registered."""
# Hide the wrapped entry if registered
registry = er.async_get(self.hass)

for entity in [CONF_ENTITY_UP, CONF_ENTITY_DOWN]:
for entity in [CONF_ENTITY_UP, CONF_ENTITY_DOWN, CONF_ENTITY_STOP]:
if not options.get(entity): # stop is optional
continue
entity_entry = registry.async_get(options[entity])
if entity_entry is not None and not entity_entry.hidden:
registry.async_update_entity(
Expand Down
1 change: 1 addition & 0 deletions custom_components/cover_time_based/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

CONF_ENTITY_UP: Final = "up"
CONF_ENTITY_DOWN: Final = "down"
CONF_ENTITY_STOP: Final = "stop"
CONF_TIME_OPEN: Final = "time_open"
CONF_TIME_CLOSE: Final = "time_close"
96 changes: 60 additions & 36 deletions custom_components/cover_time_based/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import EVENT_STATE_CHANGED
from homeassistant.const import Platform
from homeassistant.const import SERVICE_CLOSE_COVER
from homeassistant.const import SERVICE_OPEN_COVER
from homeassistant.const import SERVICE_STOP_COVER
Expand All @@ -29,6 +30,7 @@
from homeassistant.util import slugify

from .const import CONF_ENTITY_DOWN
from .const import CONF_ENTITY_STOP
from .const import CONF_ENTITY_UP
from .const import CONF_TIME_CLOSE
from .const import CONF_TIME_OPEN
Expand Down Expand Up @@ -80,6 +82,11 @@ async def async_setup_entry(
entity_down = er.async_validate_entity_id(
registry, config_entry.options[CONF_ENTITY_DOWN]
)
entity_stop = None
if config_entry.options.get(CONF_ENTITY_STOP):
entity_stop = er.async_validate_entity_id(
registry, config_entry.options[CONF_ENTITY_STOP]
)

async_add_entities(
[
Expand All @@ -90,6 +97,7 @@ async def async_setup_entry(
config_entry.options[CONF_TIME_OPEN],
entity_up,
entity_down,
entity_stop,
)
]
)
Expand All @@ -104,6 +112,7 @@ def __init__(
travel_time_up,
open_switch_entity_id,
close_switch_entity_id,
stop_switch_entity_id=None,
):
"""Initialize the cover."""
if not travel_time_down:
Expand All @@ -114,6 +123,8 @@ def __init__(
self._open_switch_entity_id = open_switch_entity_id
self._close_switch_state = STATE_OFF
self._close_switch_entity_id = close_switch_entity_id
self._stop_switch_state = STATE_OFF
self._stop_switch_entity_id = stop_switch_entity_id
self._name = name
self._attr_unique_id = unique_id

Expand Down Expand Up @@ -142,6 +153,7 @@ async def _handle_state_changed(self, event):
if event.data.get(ATTR_ENTITY_ID) not in [
self._close_switch_entity_id,
self._open_switch_entity_id,
self._stop_switch_entity_id,
]:
return

Expand All @@ -154,6 +166,13 @@ async def _handle_state_changed(self, event):
if event.data.get("new_state").state == event.data.get("old_state").state:
return

# avoid loop
if event.data.get(ATTR_ENTITY_ID).startswith("script."):
return

if event.data.get(ATTR_ENTITY_ID).startswith(f"{Platform.BUTTON}."):
return

# Target switch/light
if event.data.get(ATTR_ENTITY_ID) == self._close_switch_entity_id:
if self._close_switch_state == event.data.get("new_state").state:
Expand All @@ -163,6 +182,13 @@ async def _handle_state_changed(self, event):
if self._open_switch_state == event.data.get("new_state").state:
return
self._open_switch_state = event.data.get("new_state").state
elif (
self.has_stop_entity
and event.data.get(ATTR_ENTITY_ID) == self.stop_switch_entity_id
):
if self._stop_switch_state == event.data.get("new_state").state:
return
self._stop_switch_state = event.data.get("new_state").state

# Set unavailable if any of the switches becomes unavailable
self._attr_available = not any(
Expand Down Expand Up @@ -248,6 +274,11 @@ def assumed_state(self):
"""Return True because covers can be stopped midway."""
return True

@property
def has_stop_entity(self) -> bool:
"""Check if there is a third input used to stop the cover."""
return self._stop_switch_entity_id is not None

async def check_availability(self) -> None:
"""Check if any of the entities is unavailable and update status."""
for entity in [self._close_switch_entity_id, self._open_switch_entity_id]:
Expand Down Expand Up @@ -357,51 +388,44 @@ async def auto_stop_if_necessary(self):
await self._async_handle_command(SERVICE_STOP_COVER)
self.tc.stop()

async def set_entity(self, state: str, entity_id, wait=False):
if state not in [STATE_ON, STATE_OFF]:
raise Exception(f"calling set_entity with wrong state {state}")

domain = "homeassistant"
action = f"turn_{state}"

if entity_id.startswith(Platform.BUTTON):
domain = "input_button"
action = "press"
elif entity_id.startswith("script"):
domain = "script"

return await self.hass.services.async_call(
domain, action, {"entity_id": entity_id}, wait
)

async def _async_handle_command(self, command, *args):
if command == SERVICE_CLOSE_COVER:
self._state = False
await self.hass.services.async_call(
"homeassistant",
"turn_off",
{"entity_id": self._open_switch_entity_id},
False,
)
await self.hass.services.async_call(
"homeassistant",
"turn_on",
{"entity_id": self._close_switch_entity_id},
True,
)
if self.has_stop_entity:
await self.set_entity(STATE_OFF, self._stop_switch_entity_id)
await self.set_entity(STATE_OFF, self._open_switch_entity_id)
await self.set_entity(STATE_ON, self._close_switch_entity_id, True)

elif command == SERVICE_OPEN_COVER:
self._state = True
await self.hass.services.async_call(
"homeassistant",
"turn_off",
{"entity_id": self._close_switch_entity_id},
False,
)
await self.hass.services.async_call(
"homeassistant",
"turn_on",
{"entity_id": self._open_switch_entity_id},
True,
)
if self.has_stop_entity:
await self.set_entity(STATE_OFF, self._stop_switch_entity_id)
await self.set_entity(STATE_OFF, self._close_switch_entity_id)
await self.set_entity(STATE_ON, self._open_switch_entity_id, True)

elif command == SERVICE_STOP_COVER:
self._state = True
await self.hass.services.async_call(
"homeassistant",
"turn_off",
{"entity_id": self._close_switch_entity_id},
False,
)
await self.hass.services.async_call(
"homeassistant",
"turn_off",
{"entity_id": self._open_switch_entity_id},
False,
)
await self.set_entity(STATE_OFF, self._close_switch_entity_id)
await self.set_entity(STATE_OFF, self._open_switch_entity_id)
if self.has_stop_entity:
await self.set_entity(STATE_ON, self._stop_switch_entity_id, True)

_LOGGER.debug("_async_handle_command :: %s", command)

Expand Down
2 changes: 1 addition & 1 deletion custom_components/cover_time_based/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"integration_type": "helper",
"iot_class": "calculated",
"issue_tracker": "https://github.com/duhow/hass-cover-time-based/issues",
"version": "0.1.2"
"version": "0.2.0"
}
4 changes: 3 additions & 1 deletion custom_components/cover_time_based/translations/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
"name": "Nom",
"up": "Pujar",
"down": "Baixar",
"stop": "Aturar (opcional)",
"time_open": "Temps per obrir la persiana",
"time_close": "Temps per tancar la persiana (opcional)"
},
"data_description": {
"name": "Nom de la nova persiana a crear.",
"up": "Entitat que farà l'acció de pujar.",
"down": "Entitat que farà l'acció de baixar."
"down": "Entitat que farà l'acció de baixar.",
"stop": "Entitat que farà l'acció d'aturar el moviment."
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion custom_components/cover_time_based/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
"name": "Name",
"up": "Up",
"down": "Down",
"stop": "Stop (optional)",
"time_open": "Time to open the cover",
"time_close": "Time to close the cover (optional)"
},
"data_description": {
"name": "Name of the new cover to create.",
"up": "Entity that will open the cover.",
"down": "Entity that will close the cover."
"down": "Entity that will close the cover.",
"stop": "Entity that will stop the cover movement."
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion custom_components/cover_time_based/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
"name": "Nombre",
"up": "Subir",
"down": "Bajar",
"stop": "Parar movimiento (opcional)",
"time_open": "Tiempo para abrir la persiana",
"time_close": "Tiempo para cerrar la persiana (opcional)"
},
"data_description": {
"name": "Nombre de la nueva persiana a crear.",
"up": "Entidad que realiza la acción de subir.",
"down": "Entidad que realiza la acción de bajar."
"down": "Entidad que realiza la acción de bajar.",
"stop": "Entidad que realiza la acción de parar el movimiento."
}
}
}
Expand Down

0 comments on commit 5d1c1ff

Please sign in to comment.