Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
duhow committed Jun 27, 2024
0 parents commit e8594db
Show file tree
Hide file tree
Showing 13 changed files with 798 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
17 changes: 17 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Cover Time-based Component

Forked from [@davidramosweb](https://github.com/davidramosweb/home-assistant-custom-components-cover-time-based) @ 2021,
this custom component now integrates easily in Home Assistant.

Convert your (dummy) `switch` into a `cover`, and allow to control its position.

## 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)

## Usage

[![Open your Home Assistant instance and show your helper entities.](https://my.home-assistant.io/badges/helpers.svg)](https://my.home-assistant.io/redirect/helpers/)

Check **Change device type to a Cover time-based**.

## Credits

* [@davidramosweb](https://github.com/davidramosweb) for its original code base.
* [xknx](https://xknx.io/) Python library for the `TravelCalculator` control class.
154 changes: 154 additions & 0 deletions custom_components/cover_time_based/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Component to wrap switch entities in entities of other domains."""

from __future__ import annotations

import logging

import voluptuous as vol

from homeassistant.components.homeassistant import exposed_entities
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.event import async_track_entity_registry_updated_event

from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN
)

from .const import CONF_INVERT, CONF_TARGET_DEVICE_CLASS, CONF_ENTITY_UP, CONF_ENTITY_DOWN

_LOGGER = logging.getLogger(__name__)


@callback
def async_add_to_device(
hass: HomeAssistant, entry: ConfigEntry, entity_id: str
) -> str | None:
"""Add our config entry to the tracked entity's device."""
registry = er.async_get(hass)
device_registry = dr.async_get(hass)
device_id = None

if (
not (wrapped_switch := registry.async_get(entity_id))
or not (device_id := wrapped_switch.device_id)
or not (device_registry.async_get(device_id))
):
return device_id

device_registry.async_update_device(device_id, add_config_entry_id=entry.entry_id)

return device_id


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Check light-swich up and down exist."""
registry = er.async_get(hass)
device_registry = dr.async_get(hass)
try:
for entity in [CONF_ENTITY_UP, CONF_ENTITY_DOWN]:
entity_id = er.async_validate_entity_id(registry, entry.options[entity])
except vol.Invalid:
# The entity is identified by an unknown entity registry ID
_LOGGER.error(
"Failed to setup cover_time_based for unknown entity %s",
entry.options[entity],
)
return False

async def async_registry_updated(
event: Event[er.EventEntityRegistryUpdatedData],
) -> None:
"""Handle entity registry update."""
data = event.data
if data["action"] == "remove":
await hass.config_entries.async_remove(entry.entry_id)

if data["action"] != "update":
return

if "entity_id" in data["changes"]:
# Entity_id changed, reload the config entry
await hass.config_entries.async_reload(entry.entry_id)

if device_id and "device_id" in data["changes"]:
# If the tracked switch is no longer in the device, remove our config entry
# from the device
if (
not (entity_entry := registry.async_get(data[CONF_ENTITY_ID]))
or not device_registry.async_get(device_id)
or entity_entry.device_id == device_id
):
# No need to do any cleanup
return

device_registry.async_update_device(
device_id, remove_config_entry_id=entry.entry_id
)

entry.async_on_unload(
async_track_entity_registry_updated_event(
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)

await hass.config_entries.async_forward_entry_setups(
entry, (COVER_DOMAIN,)
)
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(
entry, (COVER_DOMAIN,)
)


async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Unload a config entry.
This will unhide the wrapped entity and restore assistant expose settings.
"""
registry = er.async_get(hass)
try:
switch_entity_id = er.async_validate_entity_id(
registry, entry.options[CONF_ENTITY_ID]
)
except vol.Invalid:
# The source entity has been removed from the entity registry
return

if not (switch_entity_entry := registry.async_get(switch_entity_id)):
return

# Unhide the wrapped entity
if switch_entity_entry.hidden_by == er.RegistryEntryHider.INTEGRATION:
registry.async_update_entity(switch_entity_id, hidden_by=None)

switch_as_x_entries = er.async_entries_for_config_entry(registry, entry.entry_id)
if not switch_as_x_entries:
return

switch_as_x_entry = switch_as_x_entries[0]

# Restore assistant expose settings
expose_settings = exposed_entities.async_get_entity_settings(
hass, switch_as_x_entry.entity_id
)
for assistant, settings in expose_settings.items():
if (should_expose := settings.get("should_expose")) is None:
continue
exposed_entities.async_expose_entity(
hass, assistant, switch_entity_id, should_expose
)
98 changes: 98 additions & 0 deletions custom_components/cover_time_based/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Config flow for Cover Time-based integration."""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

import voluptuous as vol

from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, Platform
from homeassistant.components.cover import CoverDeviceClass
from homeassistant.helpers import entity_registry as er, selector
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
SchemaFlowFormStep,
wrapped_entity_config_entry_title,
)

from .const import CONF_ENTITY_UP, CONF_ENTITY_DOWN, CONF_INVERT, CONF_TARGET_DEVICE_CLASS, CONF_TIME_OPEN, CONF_TIME_CLOSE, DOMAIN

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])
),
vol.Required(CONF_ENTITY_DOWN): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[Platform.SWITCH, Platform.LIGHT])
),
vol.Required(CONF_TIME_OPEN, default=25): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX, min=2,
max=120,
step="any",
unit_of_measurement="sec"
)
),
vol.Optional(CONF_TIME_CLOSE): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX,
max=120,
step="any",
unit_of_measurement="sec"
)
),
}
)
)
}

OPTIONS_FLOW = {
"init": SchemaFlowFormStep(
vol.Schema({
vol.Required(CONF_TIME_OPEN): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX, min=2,
max=120,
step="any",
unit_of_measurement="sec"
)
),
vol.Optional(CONF_TIME_CLOSE): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.BOX,
max=120,
step="any",
unit_of_measurement="sec"
)
),
})
),
}


class CoverTimeBasedConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for Cover Time-based."""

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."""
# Hide the wrapped entry if registered
registry = er.async_get(self.hass)

for entity in [CONF_ENTITY_UP, CONF_ENTITY_DOWN]:
entity_entry = registry.async_get(options[entity])
if entity_entry is not None and not entity_entry.hidden:
registry.async_update_entity(
options[entity], hidden_by=er.RegistryEntryHider.INTEGRATION
)

return options[CONF_NAME]
12 changes: 12 additions & 0 deletions custom_components/cover_time_based/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Constants for the Cover Time-based integration."""

from typing import Final

DOMAIN: Final = "cover_time_based"

CONF_INVERT: Final = "invert"
CONF_TARGET_DEVICE_CLASS: Final = "target_device"
CONF_ENTITY_UP: Final = "up"
CONF_ENTITY_DOWN: Final = "down"
CONF_TIME_OPEN: Final = "time_open"
CONF_TIME_CLOSE: Final = "time_close"
Loading

0 comments on commit e8594db

Please sign in to comment.