Add Tween Light Infrared integration#172803
Conversation
There was a problem hiding this comment.
This PR includes a brand folder inside the component. Brand assets should not be part of the core repository. Please refer to the brand images documentation for the correct approach.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new tween_light_ir Home Assistant integration backed by the infrared domain, including a Light platform, config flow, translations/icons, strict typing, and tests/snapshots.
Changes:
- Introduce the new
tween_light_irintegration (manifest, init/unload, config flow, constants, light platform, base entity). - Add translations (strings/icons), generated integration registries, and quality scale metadata.
- Add integration test suite (config flow, init, light behavior + snapshot) and enable strict mypy settings.
Reviewed changes
Copilot reviewed 18 out of 23 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| homeassistant/components/tween_light_ir/init.py | Sets up/unloads config entries and forwards to the Light platform. |
| homeassistant/components/tween_light_ir/config_flow.py | Adds config flow to select device type and infrared entities. |
| homeassistant/components/tween_light_ir/const.py | Defines domain constants and device type enum. |
| homeassistant/components/tween_light_ir/entity.py | Provides shared device info + unique_id behavior. |
| homeassistant/components/tween_light_ir/light.py | Implements Light entity that sends Tween Light IR codes. |
| homeassistant/components/tween_light_ir/manifest.json | Registers the integration metadata. |
| homeassistant/components/tween_light_ir/strings.json | Adds UI strings for config flow and effect translations. |
| homeassistant/components/tween_light_ir/icons.json | Adds entity/effect icon translations. |
| homeassistant/components/tween_light_ir/quality_scale.yaml | Declares quality scale rule status for the integration. |
| homeassistant/generated/integrations.json | Adds generated integration entry for tween_light_ir. |
| homeassistant/generated/config_flows.py | Adds tween_light_ir to generated config flow list. |
| mypy.ini | Enables strict mypy checks for homeassistant.components.tween_light_ir.*. |
| .strict-typing | Marks tween_light_ir as strict-typing. |
| CODEOWNERS | Assigns component and tests ownership. |
| tests/components/tween_light_ir/conftest.py | Adds test fixtures for config entries and IR code patching. |
| tests/components/tween_light_ir/test_config_flow.py | Tests config flow happy path, already configured, and missing IR entities. |
| tests/components/tween_light_ir/test_init.py | Tests entry setup and unload. |
| tests/components/tween_light_ir/test_light.py | Snapshot test + service/action tests for IR code dispatch. |
| tests/components/tween_light_ir/snapshots/test_light.ambr | Snapshot for light entity registry/state. |
| tests/components/tween_light_ir/init.py | Declares tests package. |
| self._async_abort_entries_match( | ||
| { | ||
| CONF_DEVICE_TYPE: user_input[CONF_DEVICE_TYPE], | ||
| CONF_INFRARED_ENTITY_ID: user_input.get( | ||
| CONF_INFRARED_ENTITY_ID | ||
| ), | ||
| } | ||
| ) |
| if user_input.get(CONF_INFRARED_ENTITY_ID) or user_input.get( | ||
| CONF_INFRARED_RECEIVER_ENTITY_ID | ||
| ): |
| return self.async_create_entry( | ||
| title=DEVICE_TYPE_NAMES[user_input[CONF_DEVICE_TYPE]], | ||
| data=user_input, | ||
| ) |
| if not (infrared_entity_id := entry.data.get(CONF_INFRARED_ENTITY_ID)): | ||
| return |
| if not emitter_entity_ids and not receiver_entity_ids: | ||
| return self.async_abort(reason="no_infrared_entities") | ||
|
|
||
| if user_input is not None: | ||
| if user_input.get(CONF_INFRARED_ENTITY_ID) or user_input.get( | ||
| CONF_INFRARED_RECEIVER_ENTITY_ID | ||
| ): | ||
| self._async_abort_entries_match( | ||
| { | ||
| CONF_DEVICE_TYPE: user_input[CONF_DEVICE_TYPE], | ||
| CONF_INFRARED_ENTITY_ID: user_input.get( | ||
| CONF_INFRARED_ENTITY_ID | ||
| ), | ||
| } | ||
| ) | ||
| return self.async_create_entry( | ||
| title=DEVICE_TYPE_NAMES[user_input[CONF_DEVICE_TYPE]], | ||
| data=user_input, | ||
| ) | ||
|
|
||
| errors["base"] = "missing_infrared_entity" |
| """Set up platform from config entry.""" | ||
| if not (infrared_entity_id := entry.data.get(CONF_INFRARED_ENTITY_ID)): | ||
| return | ||
|
|
||
| device_type = entry.data[CONF_DEVICE_TYPE] | ||
| if device_type == TweenLightIrDeviceType.LED_STRIP: | ||
| async_add_entities([TweenLightIrLightEntity(entry, infrared_entity_id)]) |
| return self.async_create_entry( | ||
| title=DEVICE_TYPE_NAMES[user_input[CONF_DEVICE_TYPE]], | ||
| data=user_input, | ||
| ) |
| self._attr_device_info = DeviceInfo( | ||
| identifiers={(DOMAIN, entry.entry_id)}, | ||
| model=DEVICE_TYPE_NAMES[entry.data[CONF_DEVICE_TYPE]], | ||
| manufacturer="Tween Light", | ||
| ) |
| @pytest.mark.usefixtures("mock_infrared_emitter_entity") | ||
| async def test_user_flow_requires_emitter_or_receiver( | ||
| hass: HomeAssistant, | ||
| ) -> None: | ||
| """Test user flow requires an infrared emitter or receiver.""" |
| title=DEVICE_TYPE_NAMES[user_input[CONF_DEVICE_TYPE]], | ||
| data=user_input, | ||
| ) |
| if user_input.get(CONF_INFRARED_ENTITY_ID) or user_input.get( | ||
| CONF_INFRARED_RECEIVER_ENTITY_ID | ||
| ): | ||
| self._async_abort_entries_match( | ||
| { | ||
| CONF_DEVICE_TYPE: user_input[CONF_DEVICE_TYPE], | ||
| CONF_INFRARED_ENTITY_ID: user_input.get( | ||
| CONF_INFRARED_ENTITY_ID | ||
| ), | ||
| } | ||
| ) |
| async def async_turn_on(self, **kwargs: Any) -> None: | ||
| """Turn device on.""" | ||
| command = TweenLightLEDStripCode.ON.to_command() | ||
| self._attr_is_on = True | ||
| if ATTR_EFFECT in kwargs and kwargs[ATTR_EFFECT] in self._attr_effect_list: | ||
| effect: str = kwargs[ATTR_EFFECT] | ||
| command = TweenLightLEDStripCode[effect.upper()].to_command() | ||
|
|
||
| await self._send_command(command) | ||
| self.async_write_ha_state() |
| CONF_DEVICE_TYPE: TweenLightIrDeviceType.LED_STRIP, | ||
| CONF_INFRARED_ENTITY_ID: EMITTER_ENTITY_ID, | ||
| CONF_INFRARED_RECEIVER_ENTITY_ID: RECEIVER_ENTITY_ID, |
|
As a sidenote, I am wondering how other projects like flipper call these remotes, as like you said, I recognize these remotes and there are likely a ton of brands, and I am not sure if everyone will find it as tween light |
|
Would it make sense to also call it a generic one for us? |
|
From the WLED page, I know there are like 3 or 4 variants of this remote that use different IR codes, one of them is for Osram lamps and looks exactly the same, the others look slightly different but I don't know anything about them. I know the integration will possibly work for other controllers, too, but I can't say for which ones. The only thing I know for sure is, that it will work for the Tween Light LED strip controllers. |
|
Let's throw the naming discussion in the core chat and see whats the best name to give it |
Proposed change
Tween Light is a brand marketed by the European home improvement retail chain BAUHAUS.
This integration currently only supports the Tween Light LED Strip, which comes with a 24-key infrared remote. There are other light products, like various RGB ceiling lights and LED panels, but they use different remotes for which I don't know the IR codes (actually not sure if they are IR or RF).
This 24-key remote is a pretty wide-spread model and is sold with many different LED controllers, often cheap, no-name products, which can easily be found for example on Amazon (1, 2) or Aliexpress. So this integration should be compatible with a lot of different LED controllers.

The integration requires
infrared-protocols==5.8.0Type of change
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: