diff --git a/hass.py b/hass.py new file mode 100644 index 0000000..836a89f --- /dev/null +++ b/hass.py @@ -0,0 +1,53 @@ +import mapping +import logging +import json + +logger = logging.getLogger('comfoair-hass') + +def get_friendly_name(name): + parts = name.split('_') + for i in range(len(parts)): + parts[i] = parts[i].capitalize() + return ' '.join(parts) + +def publish_hass_mqtt_discovery(mqtt_client): + logger.info("hass mqtt") + try: + for key in mapping.data: + val = mapping.data[key] + id = val.get('name') + icon = val.get('icon') + unit = val.get('unit') + friendly_name = get_friendly_name(id) + + if not val.get('ok'): + continue + if 'timer' in id: + continue + + payload = { + "name": f"comfoair_{friendly_name}", + "state_topic": f"comfoair/status/{id}", + "availability_topic": "comfoair/status", + "platform": "mqtt", + "unique_id": f"comfoair_{id}", + "device": { + "manufacturer": "Zehnder", + "model": "ComfoAir Q350", + "name": "Q350", + "identifiers": "Q350" + } + } + + if icon is not None: + payload['icon'] = icon + + if unit is not None: + payload['unit_of_measurement'] = unit + + topic_name = f"homeassistant/sensor/comfoair/{id}/config" + raw_payload = json.dumps(payload) + logger.info("publishing discovery %s %s", topic_name, raw_payload) + mqtt_client.publish(topic_name, raw_payload, 0, True) + except e: + logger.error(e) \ No newline at end of file diff --git a/main.py b/main.py index 8451e25..c58661b 100644 --- a/main.py +++ b/main.py @@ -3,12 +3,17 @@ import sys from USBCAN import CANInterface +from hass import publish_hass_mqtt_discovery import mapping from time import sleep +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + def on_mqtt_connect(client, userdata, flags, rc): logger.info('subscribing to comfoair/action') client.subscribe('comfoair/action') + client.publish('comfoair/status', 'online', 0, True) + publish_hass_mqtt_discovery(client) def on_mqtt_message(client, userdata, msg): action =str(msg.payload.decode('utf-8')) @@ -29,16 +34,17 @@ def process_can_message(pdid, data): if pdid in mapping.data: pdid_config = mapping.data[pdid] value = pdid_config.get('transformation')(data) - if pdid_config.get('ok'): + if pdid_config.get('ok') and value is not None: + #logger.info('got message for pid %s raw: %s transformed: %s', pdid, data, value) name = pdid_config.get('name') - mqtt_client.publish('comfoair/status/' + name, value) + mqtt_client.publish('comfoair/status/' + name, value, 0, True) else: logger.info('not ok, not pushing %s %s', pdid, value) elif pdid != 0: logger.info('pid not found %s %s', pdid, data) logger = logging.getLogger('comfoair-main') -logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + logger.info('starting up') mqtt_client = mqtt.Client() diff --git a/mapping.py b/mapping.py index 5a923da..009c986 100644 --- a/mapping.py +++ b/mapping.py @@ -17,6 +17,30 @@ def transform_any(value: list) -> float: word += value[n]<<(n*8) return word +def transform_away(value: list) -> bool: + val = transform_any(value) + if val == 7: + return True + if val == 1: + return False + return None + +def transform_operating_mode(value: list) -> str: + val = transform_any(value) + if val == 255: + return 'auto' + if val == 1: + return 'limited_manual' + if val == 5: + return 'unlimited_manual' + return None + +def transform_operating_mode2(value: list) -> str: + val = transform_any(value) + if val == 1: + return 'unlimited_manual' + return 'auto' + # 8415 0601 00000000 100e0000 01 Set ventilation mode: supply only for 1 hour # 8515 0601 Set ventilation mode: balance mode @@ -60,11 +84,12 @@ def transform_any(value: list) -> float: data = { 16: { - "name": "z_unknown_NwoNode", - "ok" : False, + "name": "away_indicator", + "ok" : True, "unit": "", - "transformation": transform_any - }, + "icon": "mdi:m³home-export-outline", + "transformation": transform_away + }, 17: { "name": "z_unknown_NwoNode", "ok" : False, @@ -77,17 +102,40 @@ def transform_any(value: list) -> float: "unit": "", "transformation": transform_any }, + 49: { + "name": "operating_mode", + "ok" : True, + "unit": "", + "icon": "mdi:m³brightness-auto", + "transformation": transform_operating_mode + }, + 56: { + "name": "operating_mode2", + "ok" : True, + "unit": "", + "icon": "mdi:m³brightness-auto", + "transformation": transform_operating_mode2 + }, 65: { "name": "ventilation_level", "ok" : True, - "unit": "level", + "unit": "", + "icon": "mdi:fan", "transformation": lambda x: int(x[0]) }, 66: { "name": "bypass_state", "ok" : True, - "unit": "0=auto,1=open,2=close", - "transformation": lambda x: float(x[0]) + "unit": "", + "icon": "mdi:m³gate-open", + "transformation": lambda x: ['auto', 'open', 'close'][int(x[0])] + }, + 67: { + "name": "temperature_profile", + "ok" : True, + "unit": "", + "icon": "mdi:thermometer-lines", + "transformation": lambda x: ['normal', 'cold', 'warm'][int(x[0])] }, 81: { "name": "timer_1", @@ -146,104 +194,121 @@ def transform_any(value: list) -> float: 97: { "name": "bypass_b_status", "ok" : True, - "unit": "unknown", + "icon": "mdi:gate-open", + "unit": "", "transformation": transform_air_volume }, 98: { "name": "bypass_a_status", "ok" : True, - "unit": "unknown", + "icon": "mdi:gate-open", + "unit": "", "transformation": transform_air_volume }, 115: { "name": "fan_exhaust_enabled", "ok" : True, "unit": "", + "icon": "mdi:fan-chevron-down", "transformation": transform_any }, 116: { "name": "fan_supply_enabled", "ok" : True, "unit": "", + "icon": "mdi:fan-chevron-up", "transformation": transform_any }, 117: { "name": "fan_exhaust_duty", "ok" : True, "unit": "%", + "icon": "mdi:fan-chevron-down", "transformation": lambda x: float(x[0]) }, 118: { "name": "fan_supply_duty", "ok" : True, "unit": "%", + "icon": "mdi:fan-chevron-up", "transformation": lambda x: float(x[0]) }, 119: { "name": "fan_exhaust_flow", "ok" : True, - "unit": "m3", + "unit": "m³", + "icon": "mdi:fan", "transformation": transform_air_volume }, 120: { "name": "fan_supply_flow", "ok" : True, - "unit": "m3", + "unit": "m³", + "icon": "mdi:fan", "transformation": transform_air_volume }, 121: { "name": "fan_exhaust_speed", "ok" : True, "unit": "rpm", + "icon": "mdi:speedometer", "transformation": transform_air_volume }, 122: { "name": "fan_supply_speed", "ok" : True, "unit": "rpm", + "icon": "mdi:speedometer", "transformation": transform_air_volume }, 128: { "name": "power_consumption_ventilation", "ok" : True, "unit": "W", + "icon": "mdi:power-socket-eu", "transformation": lambda x: float(x[0]) }, 129: { "name": "power_consumption_year_to_date", "ok" : True, "unit": "kWh", + "mdi": "mdi:power-plug", "transformation": transform_air_volume }, 130: { "name": "power_consumption_total_from_start", "ok" : True, "unit": "kWh", + "mdi": "mdi:power-plug", "transformation": transform_air_volume }, 144: { "name": "power_consumption_preheater_year_to_date", "ok" : True, "unit": "kWh", + "mdi": "mdi:power-plug", "transformation": transform_any }, 145: { "name": "power_consumption_preheater_from_start", "ok" : True, "unit": "kWh", + "mdi": "mdi:power-plug", "transformation": transform_any }, 146: { "name": "power_consumption_preheater_current", "ok" : True, "unit": "W", + "mdi": "mdi:power-plug", "transformation": transform_any }, 192: { "name": "days_until_next_filter_change", "ok" : True, "unit": "days", - "transformation": transform_air_volume + "mdi": "mdi:air-filter", + "transformation": transform_any }, 208: { "name": "z_Unknown_TempHumConf", @@ -255,6 +320,7 @@ def transform_any(value: list) -> float: "name" : "rmot", "ok" : True, "unit":"°C", + "icon": "mdi:thermometer-alert", "transformation":transform_temperature }, 210: { @@ -273,60 +339,70 @@ def transform_any(value: list) -> float: "name": "target_temperature", "ok" : True, "unit": "°C", + "icon": "mdi:thermometer-lines", "transformation": transform_temperature }, 213: { "name": "power_avoided_heating_actual", "ok" : True, "unit": "W", + "icon": "mdi:power-plug-off-outline", "transformation": transform_any }, 214: { "name": "power_avoided_heating_year_to_date", "ok" : True, "unit": "kWh", + "icon": "mdi:power-plug-off-outline", "transformation": transform_air_volume }, 215: { "name": "power_avoided_heating_from_start", "ok" : True, "unit": "kWh", + "icon": "mdi:power-plug-off-outline", "transformation": transform_air_volume }, 216: { "name": "power_avoided_cooling_actual", "ok" : True, "unit": "W", + "icon": "mdi:power-plug-off-outline", "transformation": transform_any }, 217: { "name": "power_avoided_cooling_year_to_date", "ok" : True, "unit": "kWh", + "icon": "mdi:power-plug-off-outline", "transformation": transform_air_volume }, 218: { "name": "power_avoided_cooling_from_start", "ok" : True, "unit": "kWh", + "icon": "mdi:power-plug-off-outline", "transformation": transform_air_volume }, 219: { "name": "power_preheater_target", "ok" : True, "unit": "W", + "icon": "mdi:power-plug", "transformation": transform_any }, 220: { "name": "air_supply_temperature_before_preheater", "ok" : True, "unit": "°C", + "icon": "mdi:thermometer", "transformation": transform_temperature }, 221: { "name": "air_supply_temperature", "ok" : True, "unit": "°C", + "icon": "mdi:thermometer", "transformation": transform_temperature }, 222: { @@ -386,36 +462,42 @@ def transform_any(value: list) -> float: 273: { "name": "temperature_unknown", "unit": "°C", + "ok": False, "transformation": transform_temperature }, 274: { "name": "air_extract_temperature", "ok" : True, "unit": "°C", + "icon": "mdi:home-thermometer-outline", "transformation": transform_temperature }, 275: { "name": "air_exhaust_temperature", "ok" : True, "unit": "°C", + "icon": "mdi:home-thermometer-outline", "transformation": transform_temperature }, 276: { "name": "air_outdoor_temperature_before_preheater", "ok" : True, "unit": "°C", + "icon": "mdi:thermometer", "transformation": transform_temperature }, 277: { "name": "air_outdoor_temperature", "ok" : True, "unit": "°C", + "icon": "mdi:thermometer", "transformation": transform_temperature }, 278: { "name": "air_supply_temperature_2", "ok" : True, "unit": "°C", + "icon": "mdi:thermometer", "transformation": transform_temperature }, 289: { @@ -427,30 +509,35 @@ def transform_any(value: list) -> float: "name": "air_extract_humidity", "ok" : True, "unit": "%", + "icon": "mdi:water-percent", "transformation": lambda x: float(x[0]) }, 291: { "name": "air_exhaust_humidity", "ok" : True, - "unit": "%", + "unit": "%", + "icon": "mdi:water-percent", "transformation": lambda x: float(x[0]) }, 292: { "name": "air_outdoor_humidity_before_preheater", "ok" : True, "unit": "%", + "icon": "mdi:water-percent", "transformation": lambda x: float(x[0]) }, 293: { "name": "air_outdoor_humidity", "ok" : True, "unit": "%", + "icon": "mdi:water-percent", "transformation": lambda x: float(x[0]) }, 294: { "name": "air_supply_humidity", "ok" : True, "unit": "%", + "icon": "mdi:water-percent", "transformation": lambda x: float(x[0]) }, @@ -458,12 +545,14 @@ def transform_any(value: list) -> float: "name": "pressure_exhaust", "ok" : True, "unit": "Pa", + "icon": "mdi:package-down", "transformation": transform_any }, 306: { "name": "pressure_supply", "ok" : True, "unit": "Pa", + "icon": "mdi:package-down", "transformation": transform_any }, diff --git a/openhab/items.py b/openhab/items.py index db3aed3..992485b 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -3,7 +3,6 @@ from mapping import data - def get_friendly_name(name): parts = name.split('_') for i in range(len(parts)): @@ -20,15 +19,40 @@ def get_format(name, unit): return '%.0f °C' return '%d' -for key in data: - val = data[key] - name = val.get('name') - if not val.get('ok'): - continue - if 'timer' in name: - continue - id = get_id(name) - friendly_name = get_friendly_name(name) - format = get_format(name, val.get('unit')) - item = 'Number ComfoAir_{} "{} [{}]" (gPersistEveryMin) {{ mqtt="<[mosquitto:comfoair/status/{}:state:default]" }}'.format(id, friendly_name, format, name) - print(item) \ No newline at end of file +def get_filtered_items(): + for key in data: + val = data[key] + name = val.get('name') + if not val.get('ok'): + continue + if 'timer' in name: + continue + id = get_id(name) + friendly_name = get_friendly_name(name) + format = get_format(name, val.get('unit')) + + yield id, name, friendly_name, format, + +def print_items(): + for id, name, friendly_name, format in get_filtered_items(): + item = 'Number ComfoAir_{} "{} [{}]" (gPersistEveryMin) {{ channel="mqtt:topic:comfoair:status:{}" }}'.format(id, friendly_name, format, name) + print(item) + +def print_things(): + print(f""" + Thing mqtt:topic:comfoair:status "Comfoair Status" (mqtt:broker:main) {{ + Channels: + """) + + for id, name, friendly_name, format in get_filtered_items(): + print(f""" + Type number: {name} "{friendly_name}" [ + stateTopic="comfoair/status/{name}" + ] + """) + + print("}") + +print_things() + +print_items() \ No newline at end of file