Skip to content

Commit 963953e

Browse files
committed
Add Tween Light Infrared integration
1 parent 71eefdc commit 963953e

20 files changed

Lines changed: 902 additions & 0 deletions

File tree

.strict-typing

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ homeassistant.components.transmission.*
594594
homeassistant.components.trend.*
595595
homeassistant.components.trmnl.*
596596
homeassistant.components.tts.*
597+
homeassistant.components.tween_light_ir.*
597598
homeassistant.components.twentemilieu.*
598599
homeassistant.components.unifi.*
599600
homeassistant.components.unifi_access.*

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""The Tween Light Infrared integration."""
2+
3+
from homeassistant.config_entries import ConfigEntry
4+
from homeassistant.const import Platform
5+
from homeassistant.core import HomeAssistant
6+
7+
PLATFORMS: list[Platform] = [Platform.LIGHT]
8+
9+
10+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
11+
"""Set up Tween Light Infrared from a config entry."""
12+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
13+
return True
14+
15+
16+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
17+
"""Unload a config entry."""
18+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Config flow for the Tween Light Infrared integration."""
2+
3+
from typing import Any
4+
5+
import voluptuous as vol
6+
7+
from homeassistant.components.infrared import (
8+
DOMAIN as INFRARED_DOMAIN,
9+
async_get_emitters,
10+
async_get_receivers,
11+
)
12+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
13+
from homeassistant.helpers.selector import (
14+
EntitySelector,
15+
EntitySelectorConfig,
16+
SelectSelector,
17+
SelectSelectorConfig,
18+
SelectSelectorMode,
19+
)
20+
21+
from .const import (
22+
CONF_DEVICE_TYPE,
23+
CONF_INFRARED_ENTITY_ID,
24+
CONF_INFRARED_RECEIVER_ENTITY_ID,
25+
DEVICE_TYPE_NAMES,
26+
DOMAIN,
27+
TweenLightIrDeviceType,
28+
)
29+
30+
31+
class TweenLightIrConfigFlow(ConfigFlow, domain=DOMAIN):
32+
"""Handle a config flow for Tween Light Infrared."""
33+
34+
async def async_step_user(
35+
self, user_input: dict[str, Any] | None = None
36+
) -> ConfigFlowResult:
37+
"""Handle the initial step."""
38+
errors: dict[str, str] = {}
39+
emitter_entity_ids = async_get_emitters(self.hass)
40+
receiver_entity_ids = async_get_receivers(self.hass)
41+
if not emitter_entity_ids and not receiver_entity_ids:
42+
return self.async_abort(reason="no_infrared_entities")
43+
44+
if user_input is not None:
45+
if user_input.get(CONF_INFRARED_ENTITY_ID) or user_input.get(
46+
CONF_INFRARED_RECEIVER_ENTITY_ID
47+
):
48+
self._async_abort_entries_match(
49+
{
50+
CONF_DEVICE_TYPE: user_input[CONF_DEVICE_TYPE],
51+
CONF_INFRARED_ENTITY_ID: user_input.get(
52+
CONF_INFRARED_ENTITY_ID
53+
),
54+
}
55+
)
56+
return self.async_create_entry(
57+
title=DEVICE_TYPE_NAMES[user_input[CONF_DEVICE_TYPE]],
58+
data=user_input,
59+
)
60+
61+
errors["base"] = "missing_infrared_entity"
62+
63+
return self.async_show_form(
64+
step_id="user",
65+
data_schema=vol.Schema(
66+
{
67+
vol.Required(CONF_DEVICE_TYPE): SelectSelector(
68+
SelectSelectorConfig(
69+
options=[
70+
device_type.value
71+
for device_type in TweenLightIrDeviceType
72+
],
73+
translation_key=CONF_DEVICE_TYPE,
74+
mode=SelectSelectorMode.DROPDOWN,
75+
)
76+
),
77+
vol.Optional(CONF_INFRARED_ENTITY_ID): EntitySelector(
78+
EntitySelectorConfig(
79+
domain=INFRARED_DOMAIN,
80+
include_entities=emitter_entity_ids,
81+
)
82+
),
83+
vol.Optional(CONF_INFRARED_RECEIVER_ENTITY_ID): EntitySelector(
84+
EntitySelectorConfig(
85+
domain=INFRARED_DOMAIN,
86+
include_entities=receiver_entity_ids,
87+
)
88+
),
89+
}
90+
),
91+
errors=errors,
92+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Constants for the Tween Light Infrared integration."""
2+
3+
from enum import StrEnum
4+
5+
DOMAIN = "tween_light_ir"
6+
CONF_INFRARED_ENTITY_ID = "infrared_entity_id"
7+
CONF_INFRARED_RECEIVER_ENTITY_ID = "infrared_receiver_entity_id"
8+
CONF_DEVICE_TYPE = "device_type"
9+
10+
11+
class TweenLightIrDeviceType(StrEnum):
12+
"""Tween Light Infrared device types."""
13+
14+
LED_STRIP = "led_strip"
15+
16+
17+
DEVICE_TYPE_NAMES: dict[TweenLightIrDeviceType, str] = {
18+
TweenLightIrDeviceType.LED_STRIP: "LED Strip",
19+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Common entity for Tween Light Infrared integration."""
2+
3+
from homeassistant.config_entries import ConfigEntry
4+
from homeassistant.helpers.device_registry import DeviceInfo
5+
from homeassistant.helpers.entity import Entity
6+
7+
from .const import CONF_DEVICE_TYPE, DEVICE_TYPE_NAMES, DOMAIN
8+
9+
10+
class TweenLightIrEntity(Entity):
11+
"""Tween Light Infrared base entity providing common device info."""
12+
13+
_attr_has_entity_name = True
14+
15+
def __init__(self, entry: ConfigEntry, unique_id_suffix: str | None = None) -> None:
16+
"""Initialize entity."""
17+
self._attr_unique_id = (
18+
f"{entry.entry_id}_{unique_id_suffix}"
19+
if unique_id_suffix is not None
20+
else entry.entry_id
21+
)
22+
self._attr_device_info = DeviceInfo(
23+
identifiers={(DOMAIN, entry.entry_id)},
24+
model=DEVICE_TYPE_NAMES[entry.data[CONF_DEVICE_TYPE]],
25+
manufacturer="Tween Light",
26+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"entity": {
3+
"light": {
4+
"led_strip": {
5+
"default": "mdi:led-strip-variant",
6+
"state": {
7+
"off": "mdi:led-strip-variant-off"
8+
},
9+
"state_attributes": {
10+
"effect": {
11+
"state": {
12+
"blue": "mdi:palette",
13+
"cyan": "mdi:palette",
14+
"dark_cyan": "mdi:palette",
15+
"fade": "mdi:gradient-horizontal",
16+
"flash": "mdi:flash",
17+
"green": "mdi:palette",
18+
"light_green": "mdi:palette",
19+
"orange": "mdi:palette",
20+
"orange_red": "mdi:palette",
21+
"plum": "mdi:palette",
22+
"purple": "mdi:palette",
23+
"rebecca_purple": "mdi:palette",
24+
"red": "mdi:palette",
25+
"sky_blue": "mdi:palette",
26+
"smooth": "mdi:looks",
27+
"strobe": "mdi:light-flood-down",
28+
"tomato": "mdi:palette",
29+
"turquoise": "mdi:palette",
30+
"white": "mdi:palette",
31+
"yellow": "mdi:palette"
32+
}
33+
}
34+
}
35+
}
36+
}
37+
}
38+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Light platform for Tween Light Infrared integration."""
2+
3+
from typing import Any
4+
5+
from infrared_protocols.codes.tween_light.led_strip import TweenLightLEDStripCode
6+
7+
from homeassistant.components.infrared import InfraredEmitterConsumerEntity
8+
from homeassistant.components.light import (
9+
ATTR_EFFECT,
10+
ColorMode,
11+
LightEntity,
12+
LightEntityFeature,
13+
)
14+
from homeassistant.config_entries import ConfigEntry
15+
from homeassistant.core import HomeAssistant
16+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
17+
18+
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, TweenLightIrDeviceType
19+
from .entity import TweenLightIrEntity
20+
21+
PARALLEL_UPDATES = 1
22+
23+
24+
async def async_setup_entry(
25+
hass: HomeAssistant,
26+
entry: ConfigEntry,
27+
async_add_entities: AddConfigEntryEntitiesCallback,
28+
) -> None:
29+
"""Set up platform from config entry."""
30+
if not (infrared_entity_id := entry.data.get(CONF_INFRARED_ENTITY_ID)):
31+
return
32+
33+
device_type = entry.data[CONF_DEVICE_TYPE]
34+
if device_type == TweenLightIrDeviceType.LED_STRIP:
35+
async_add_entities([TweenLightIrLightEntity(entry, infrared_entity_id)])
36+
37+
38+
class TweenLightIrLightEntity(
39+
TweenLightIrEntity, InfraredEmitterConsumerEntity, LightEntity
40+
):
41+
"""Represents a Tween Light Infrared light entity."""
42+
43+
_attr_assumed_state = True
44+
_attr_color_mode = ColorMode.ONOFF
45+
_attr_effect_list = [
46+
"flash",
47+
"strobe",
48+
"fade",
49+
"smooth",
50+
"red",
51+
"green",
52+
"blue",
53+
"white",
54+
"orange_red",
55+
"tomato",
56+
"light_green",
57+
"sky_blue",
58+
"cyan",
59+
"rebecca_purple",
60+
"orange",
61+
"turquoise",
62+
"purple",
63+
"yellow",
64+
"dark_cyan",
65+
"plum",
66+
]
67+
_attr_name = None
68+
_attr_supported_color_modes = {ColorMode.ONOFF}
69+
_attr_supported_features = LightEntityFeature.EFFECT
70+
_attr_translation_key = "led_strip"
71+
72+
def __init__(self, entry: ConfigEntry, infrared_entity_id: str) -> None:
73+
"""Initialize the entity."""
74+
super().__init__(entry)
75+
self._infrared_emitter_entity_id = infrared_entity_id
76+
77+
async def async_turn_on(self, **kwargs: Any) -> None:
78+
"""Turn device on."""
79+
command = TweenLightLEDStripCode.ON.to_command()
80+
self._attr_is_on = True
81+
if ATTR_EFFECT in kwargs and kwargs[ATTR_EFFECT] in self._attr_effect_list:
82+
effect: str = kwargs[ATTR_EFFECT]
83+
command = TweenLightLEDStripCode[effect.upper()].to_command()
84+
self._attr_effect = effect
85+
86+
await self._send_command(command)
87+
self.async_write_ha_state()
88+
89+
async def async_turn_off(self, **kwargs: Any) -> None:
90+
"""Turn the entity off."""
91+
self._attr_is_on = False
92+
await self._send_command(TweenLightLEDStripCode.OFF.to_command())
93+
self.async_write_ha_state()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"domain": "tween_light_ir",
3+
"name": "Tween Light Infrared",
4+
"codeowners": ["@tr4nt0r"],
5+
"config_flow": true,
6+
"documentation": "https://www.home-assistant.io/integrations/tween_light_ir",
7+
"integration_type": "device",
8+
"iot_class": "assumed_state",
9+
"quality_scale": "bronze"
10+
}

0 commit comments

Comments
 (0)