mirror of
https://github.com/bahmcloud/HA-KNX-Bridge.git
synced 2026-04-07 06:51:13 +00:00
Compare commits
2 Commits
v0.0.1
...
127fa98471
| Author | SHA1 | Date | |
|---|---|---|---|
| 127fa98471 | |||
| b733f9d62d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
|||||||
.idea/
|
.idea/
|
||||||
|
!.idea/START_PROMPT.md
|
||||||
|
!.idea/PROJECT_STATE.md
|
||||||
.DS_Store
|
.DS_Store
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
55
.idea/PROJECT_STATE.md
generated
Normal file
55
.idea/PROJECT_STATE.md
generated
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Project State: HA KNX Bridge
|
||||||
|
|
||||||
|
## Project Description
|
||||||
|
HA KNX Bridge is a Home Assistant custom integration that mirrors Home Assistant entities
|
||||||
|
to KNX group addresses and accepts KNX actions to control Home Assistant entities.
|
||||||
|
It is installed via HACS and configured through the Home Assistant UI (config flow).
|
||||||
|
The integration should:
|
||||||
|
- Reuse an existing Home Assistant KNX integration entry when available.
|
||||||
|
- Allow users to add "Ports" that bind a single HA entity to KNX group addresses.
|
||||||
|
- Auto-select appropriate KNX DPTs and request only the group addresses needed.
|
||||||
|
- Ignore missing/empty group addresses without errors.
|
||||||
|
|
||||||
|
Target compatibility: Home Assistant 2025.12 with forward compatibility to 2026.2.
|
||||||
|
|
||||||
|
Project rules:
|
||||||
|
- Keep `README.md` updated for user-relevant changes after each new version.
|
||||||
|
- Maintain `CHANGELOG.md` with newest entries at the top.
|
||||||
|
- Releases are created only when explicitly requested.
|
||||||
|
- Version number must match everywhere it is referenced (manifest, changelog, README, HACS if used).
|
||||||
|
|
||||||
|
## Current State (as of 2026-02-13)
|
||||||
|
Completed:
|
||||||
|
- Repo initialized with `main` branch and pushed to GitHub.
|
||||||
|
- HACS metadata (`hacs.json`) and base integration scaffold created.
|
||||||
|
- Config flow created with subentries ("Ports") for:
|
||||||
|
- Binary Sensor
|
||||||
|
- Cover
|
||||||
|
- KNX group address validation and normalization in subentry flows.
|
||||||
|
- Options flow skeleton added (no options yet).
|
||||||
|
- `bcs.yaml` metadata file added for BCS store listing.
|
||||||
|
- Bridge manager implements:
|
||||||
|
- Binary sensor state -> KNX send (DPT 1)
|
||||||
|
- Cover KNX incoming commands -> HA services
|
||||||
|
- Cover HA state -> KNX percent updates (DPT 5.001)
|
||||||
|
- README includes initial DPT mapping and roadmap.
|
||||||
|
- Project version set to 0.0.2 and `CHANGELOG.md` maintained.
|
||||||
|
|
||||||
|
Files created:
|
||||||
|
- `custom_components/ha_knx_bridge/__init__.py`
|
||||||
|
- `custom_components/ha_knx_bridge/bridge.py`
|
||||||
|
- `custom_components/ha_knx_bridge/config_flow.py`
|
||||||
|
- `custom_components/ha_knx_bridge/const.py`
|
||||||
|
- `custom_components/ha_knx_bridge/manifest.json`
|
||||||
|
- `custom_components/ha_knx_bridge/strings.json`
|
||||||
|
- `hacs.json`
|
||||||
|
- `README.md`
|
||||||
|
- `CHANGELOG.md`
|
||||||
|
- `.gitignore`
|
||||||
|
|
||||||
|
Open items / next steps:
|
||||||
|
- Validate and format KNX group address inputs in config flow.
|
||||||
|
- Add optional option flow for future settings.
|
||||||
|
- Expand entity coverage (light, switch, sensor, climate).
|
||||||
|
- Add tests (config flow, KNX mapping).
|
||||||
|
- Improve DPT auto-selection logic per entity features.
|
||||||
20
.idea/START_PROMPT.md
generated
Normal file
20
.idea/START_PROMPT.md
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Start Prompt (Use for Every New Chat)
|
||||||
|
|
||||||
|
You are working on the `HA-KNX-Bridge` project.
|
||||||
|
|
||||||
|
Required steps at the start of every new chat:
|
||||||
|
1. Read `.idea/PROJECT_STATE.md` to understand the goal and current status.
|
||||||
|
2. Scan the repository for recent code changes relevant to the request.
|
||||||
|
3. If you make changes, update `.idea/PROJECT_STATE.md` to reflect the new state and date.
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
- Keep the project description and state accurate and concise.
|
||||||
|
- Always write an explicit date (YYYY-MM-DD) when updating the Current State section.
|
||||||
|
- Do not invent progress; only record what is verified in the repo.
|
||||||
|
- After any user-relevant change and version bump, update `README.md`.
|
||||||
|
- Maintain `CHANGELOG.md` with newest entries at the top, format:
|
||||||
|
- Version number
|
||||||
|
- Date (YYYY-MM-DD)
|
||||||
|
- Bullet list of changes
|
||||||
|
- Create releases only when explicitly requested.
|
||||||
|
- Keep the version number consistent in all files where it appears.
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.0.2 - 2026-02-13
|
||||||
|
- Validate and normalize KNX group addresses in subentry config flows.
|
||||||
|
- Add options flow skeleton for future settings.
|
||||||
|
- Add `bcs.yaml` metadata for BCS store listing.
|
||||||
|
|
||||||
## 0.0.1 - 2026-02-13
|
## 0.0.1 - 2026-02-13
|
||||||
- Initial HACS-ready scaffold with config flow and subentries for binary sensor and cover.
|
- Initial HACS-ready scaffold with config flow and subentries for binary sensor and cover.
|
||||||
- KNX bridge logic for basic send/receive mappings.
|
- KNX bridge logic for basic send/receive mappings.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ Current minimal scope:
|
|||||||
- `angle_state_address` (DPT 5.001): KNX group address that receives HA tilt updates.
|
- `angle_state_address` (DPT 5.001): KNX group address that receives HA tilt updates.
|
||||||
|
|
||||||
If a group address is left empty, it is ignored.
|
If a group address is left empty, it is ignored.
|
||||||
|
Group address format must be `X/Y/Z` (0-31/0-7/0-255) or `X/Y` (0-31/0-2047).
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- For DPT 1.008 (Up/Down), the bridge treats `0 = Up/Open` and `1 = Down/Close`.
|
- For DPT 1.008 (Up/Down), the bridge treats `0 = Up/Open` and `1 = Down/Close`.
|
||||||
@@ -46,6 +47,6 @@ If a group address is left empty, it is ignored.
|
|||||||
- Advanced DPT mapping options and inversion settings.
|
- Advanced DPT mapping options and inversion settings.
|
||||||
|
|
||||||
## Versioning and Releases
|
## Versioning and Releases
|
||||||
- Current version: 0.0.1
|
- Current version: 0.0.2
|
||||||
- `CHANGELOG.md` lists versions with the newest entries at the top.
|
- `CHANGELOG.md` lists versions with the newest entries at the top.
|
||||||
- Release creation is manual and only done when explicitly requested.
|
- Release creation is manual and only done when explicitly requested.
|
||||||
|
|||||||
21
bcs.yaml
Normal file
21
bcs.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: HA KNX Bridge
|
||||||
|
description: >
|
||||||
|
Home Assistant custom integration that mirrors Home Assistant entities
|
||||||
|
to KNX group addresses and accepts KNX actions to control Home Assistant
|
||||||
|
entities. It reuses an existing Home Assistant KNX integration and
|
||||||
|
provides per-entity "Ports" with automatic DPT selection and UI setup
|
||||||
|
via config flow.
|
||||||
|
|
||||||
|
category: Integrations
|
||||||
|
|
||||||
|
author: Bahmcloud
|
||||||
|
maintainer: Bahmcloud
|
||||||
|
|
||||||
|
domains:
|
||||||
|
- ha_knx_bridge
|
||||||
|
|
||||||
|
min_ha_version: "2025.12.0"
|
||||||
|
|
||||||
|
homepage: https://github.com/bahmcloud/HA-KNX-Bridge
|
||||||
|
issues: https://github.com/bahmcloud/HA-KNX-Bridge/issues
|
||||||
|
source: https://github.com/bahmcloud/HA-KNX-Bridge
|
||||||
@@ -54,6 +54,11 @@ class HAKnxBridgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
return self.async_show_form(step_id="user", data_schema=schema)
|
return self.async_show_form(step_id="user", data_schema=schema)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
return HAKnxBridgeOptionsFlow(config_entry)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_supported_subentry_types(config_entry):
|
def async_get_supported_subentry_types(config_entry):
|
||||||
@@ -67,15 +72,72 @@ class HAKnxBridgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HAKnxBridgeOptionsFlow(config_entries.OptionsFlow):
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
|
self._config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input: dict | None = None):
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
schema = vol.Schema({})
|
||||||
|
return self.async_show_form(step_id="init", data_schema=schema)
|
||||||
|
|
||||||
|
|
||||||
class BinarySensorPortSubentryFlow(config_entries.ConfigSubentryFlow):
|
class BinarySensorPortSubentryFlow(config_entries.ConfigSubentryFlow):
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def async_step_user(self, user_input: dict | None = None):
|
async def async_step_user(self, user_input: dict | None = None):
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
user_input, errors = _validate_knx_addresses(
|
||||||
|
user_input, [CONF_STATE_ADDRESS]
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=_binary_sensor_schema(), errors=errors
|
||||||
|
)
|
||||||
title = _entity_title(self.hass, user_input[CONF_ENTITY_ID])
|
title = _entity_title(self.hass, user_input[CONF_ENTITY_ID])
|
||||||
return self.async_create_entry(title=title, data=user_input)
|
return self.async_create_entry(title=title, data=user_input)
|
||||||
|
|
||||||
schema = vol.Schema(
|
return self.async_show_form(step_id="user", data_schema=_binary_sensor_schema())
|
||||||
|
|
||||||
|
|
||||||
|
class CoverPortSubentryFlow(config_entries.ConfigSubentryFlow):
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input: dict | None = None):
|
||||||
|
if user_input is not None:
|
||||||
|
user_input, errors = _validate_knx_addresses(
|
||||||
|
user_input,
|
||||||
|
[
|
||||||
|
CONF_MOVE_LONG_ADDRESS,
|
||||||
|
CONF_MOVE_SHORT_ADDRESS,
|
||||||
|
CONF_STOP_ADDRESS,
|
||||||
|
CONF_POSITION_ADDRESS,
|
||||||
|
CONF_POSITION_STATE_ADDRESS,
|
||||||
|
CONF_ANGLE_ADDRESS,
|
||||||
|
CONF_ANGLE_STATE_ADDRESS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=_cover_schema(), errors=errors
|
||||||
|
)
|
||||||
|
title = _entity_title(self.hass, user_input[CONF_ENTITY_ID])
|
||||||
|
return self.async_create_entry(title=title, data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="user", data_schema=_cover_schema())
|
||||||
|
|
||||||
|
|
||||||
|
def _entity_title(hass, entity_id: str) -> str:
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
if state is None:
|
||||||
|
return entity_id
|
||||||
|
return state.attributes.get("friendly_name", entity_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _binary_sensor_schema() -> vol.Schema:
|
||||||
|
return vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(domain=["binary_sensor"])
|
selector.EntitySelectorConfig(domain=["binary_sensor"])
|
||||||
@@ -85,18 +147,10 @@ class BinarySensorPortSubentryFlow(config_entries.ConfigSubentryFlow):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.async_show_form(step_id="user", data_schema=schema)
|
|
||||||
|
|
||||||
|
|
||||||
class CoverPortSubentryFlow(config_entries.ConfigSubentryFlow):
|
def _cover_schema() -> vol.Schema:
|
||||||
VERSION = 1
|
return vol.Schema(
|
||||||
|
|
||||||
async def async_step_user(self, user_input: dict | None = None):
|
|
||||||
if user_input is not None:
|
|
||||||
title = _entity_title(self.hass, user_input[CONF_ENTITY_ID])
|
|
||||||
return self.async_create_entry(title=title, data=user_input)
|
|
||||||
|
|
||||||
schema = vol.Schema(
|
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||||
selector.EntitySelectorConfig(domain=["cover"])
|
selector.EntitySelectorConfig(domain=["cover"])
|
||||||
@@ -124,11 +178,56 @@ class CoverPortSubentryFlow(config_entries.ConfigSubentryFlow):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.async_show_form(step_id="user", data_schema=schema)
|
|
||||||
|
|
||||||
|
|
||||||
def _entity_title(hass, entity_id: str) -> str:
|
def _validate_knx_addresses(
|
||||||
state = hass.states.get(entity_id)
|
user_input: dict, keys: list[str]
|
||||||
if state is None:
|
) -> tuple[dict, dict[str, str]]:
|
||||||
return entity_id
|
errors: dict[str, str] = {}
|
||||||
return state.attributes.get("friendly_name", entity_id)
|
cleaned = dict(user_input)
|
||||||
|
for key in keys:
|
||||||
|
if key not in cleaned:
|
||||||
|
continue
|
||||||
|
value = cleaned.get(key)
|
||||||
|
if value is None:
|
||||||
|
cleaned.pop(key, None)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
normalized = _normalize_group_address(str(value))
|
||||||
|
except ValueError:
|
||||||
|
errors[key] = "invalid_ga"
|
||||||
|
continue
|
||||||
|
if normalized == "":
|
||||||
|
cleaned.pop(key, None)
|
||||||
|
else:
|
||||||
|
cleaned[key] = normalized
|
||||||
|
return cleaned, errors
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_group_address(value: str) -> str:
|
||||||
|
text = value.strip()
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
parts = text.split("/")
|
||||||
|
if len(parts) == 2:
|
||||||
|
main, sub = _parse_int(parts[0]), _parse_int(parts[1])
|
||||||
|
if not (0 <= main <= 31 and 0 <= sub <= 2047):
|
||||||
|
raise ValueError("group address out of range")
|
||||||
|
return f"{main}/{sub}"
|
||||||
|
if len(parts) == 3:
|
||||||
|
main, middle, sub = (
|
||||||
|
_parse_int(parts[0]),
|
||||||
|
_parse_int(parts[1]),
|
||||||
|
_parse_int(parts[2]),
|
||||||
|
)
|
||||||
|
if not (0 <= main <= 31 and 0 <= middle <= 7 and 0 <= sub <= 255):
|
||||||
|
raise ValueError("group address out of range")
|
||||||
|
return f"{main}/{middle}/{sub}"
|
||||||
|
raise ValueError("invalid group address format")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_int(value: str) -> int:
|
||||||
|
text = value.strip()
|
||||||
|
if text == "":
|
||||||
|
raise ValueError("empty group address part")
|
||||||
|
return int(text)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "ha_knx_bridge",
|
"domain": "ha_knx_bridge",
|
||||||
"name": "HA KNX Bridge",
|
"name": "HA KNX Bridge",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://github.com/bahmcloud/HA-KNX-Bridge",
|
"documentation": "https://github.com/bahmcloud/HA-KNX-Bridge",
|
||||||
"issue_tracker": "https://github.com/bahmcloud/HA-KNX-Bridge/issues",
|
"issue_tracker": "https://github.com/bahmcloud/HA-KNX-Bridge/issues",
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"knx_not_configured": "Set up the Home Assistant KNX integration first."
|
"knx_not_configured": "Set up the Home Assistant KNX integration first."
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_ga": "Invalid KNX group address. Use X/Y/Z (0-31/0-7/0-255) or X/Y (0-31/0-2047)."
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "HA KNX Bridge",
|
"title": "HA KNX Bridge",
|
||||||
@@ -13,7 +16,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "HA KNX Bridge Options",
|
||||||
|
"description": "No options available yet."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"config_subentries": {
|
"config_subentries": {
|
||||||
|
"error": {
|
||||||
|
"invalid_ga": "Invalid KNX group address. Use X/Y/Z (0-31/0-7/0-255) or X/Y (0-31/0-2047)."
|
||||||
|
},
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
|||||||
Reference in New Issue
Block a user