Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: duhow/hass-cover-time-based
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.1.2
Choose a base ref
...
head repository: duhow/hass-cover-time-based
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 3 commits
  • 12 files changed
  • 1 contributor

Commits on Sep 6, 2024

  1. feat: stop entity, add support for button and script (#4)

    * add optional stop entity
    
    * add script and button to integration
    duhow authored Sep 6, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    Excellify Excellify
    Copy the full SHA
    5d1c1ff View commit details

Commits on Sep 8, 2024

  1. Create FUNDING.yml

    duhow authored Sep 8, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    Excellify Excellify
    Copy the full SHA
    c27df50 View commit details

Commits on Nov 8, 2024

  1. chore: update pre-commit checks

    duhow committed Nov 8, 2024
    Copy the full SHA
    e771c07 View commit details
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
ko_fi: duhow
custom: [https://paypal.me/duhow]
19 changes: 8 additions & 11 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ default_install_hook_types:
- commit-msg
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: check-docstring-first
@@ -14,38 +14,35 @@ repos:
args: [--autofix, --no-sort-keys]
- id: check-added-large-files
- id: check-yaml
exclude: ^.github/FUNDING.yml$
- id: debug-statements
- id: end-of-file-fixer
- repo: https://github.com/myint/docformatter
rev: v1.5.1
rev: eb1df347edd128b30cd3368dddc3aa65edcfac38
hooks:
- id: docformatter
language: python
args: [--in-place]
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.19.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0
hooks:
- id: reorder-python-imports
args: [--py38-plus]
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 24.10.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
rev: 7.1.1
hooks:
- id: flake8
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: python-use-type-annotations
- repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt
rev: 0.2.2
rev: 0.2.3
hooks:
- id: yamlfmt
args: [--mapping, '2', --sequence, '2', --offset, '0']
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions custom_components/cover_time_based/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Component to wrap switch entities in entities of other domains."""

from __future__ import annotations

import logging
21 changes: 13 additions & 8 deletions custom_components/cover_time_based/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Config flow for Cover Time-based integration."""

from __future__ import annotations

from collections.abc import Mapping
@@ -13,25 +14,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(
@@ -89,15 +92,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(
2 changes: 2 additions & 0 deletions custom_components/cover_time_based/const.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Constants for the Cover Time-based integration."""

from typing import Final

DOMAIN: Final = "cover_time_based"

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"
97 changes: 61 additions & 36 deletions custom_components/cover_time_based/cover.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Cover support for switch entities."""

from __future__ import annotations

import logging
@@ -11,6 +12,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
@@ -29,6 +31,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
@@ -80,6 +83,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(
[
@@ -90,6 +98,7 @@ async def async_setup_entry(
config_entry.options[CONF_TIME_OPEN],
entity_up,
entity_down,
entity_stop,
)
]
)
@@ -104,6 +113,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:
@@ -114,6 +124,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

@@ -142,6 +154,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

@@ -154,6 +167,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:
@@ -163,6 +183,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(
@@ -248,6 +275,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]:
@@ -357,51 +389,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)

2 changes: 1 addition & 1 deletion custom_components/cover_time_based/manifest.json
Original file line number Diff line number Diff line change
@@ -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
@@ -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."
}
}
}
4 changes: 3 additions & 1 deletion custom_components/cover_time_based/translations/en.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
4 changes: 3 additions & 1 deletion custom_components/cover_time_based/translations/es.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
Loading