Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
af373df
use device name in topics. Correct errors for groups. Allow group def…
binsentsu Mar 28, 2024
4208a85
treat groups and lamps the same in get_lamp_object
binsentsu Mar 28, 2024
ca1fbfc
docker file
binsentsu Jul 17, 2024
0322041
use lower/upper limit iso value error
binsentsu Jul 17, 2024
507bd96
dont check boundaries
binsentsu Jul 17, 2024
e055504
dont check boundaries
binsentsu Jul 17, 2024
89d93ba
dont check boundaries
binsentsu Jul 17, 2024
a64dc2b
update
binsentsu Jul 17, 2024
421fd26
update
binsentsu Jul 17, 2024
f0b35fa
update
binsentsu Jul 17, 2024
99c3611
update
binsentsu Jul 17, 2024
c4183a6
update
binsentsu Jul 17, 2024
a46c67a
update
binsentsu Jul 17, 2024
2347428
update
binsentsu Jul 17, 2024
9e190d4
update
binsentsu Jul 17, 2024
a8c6125
update
binsentsu Jul 17, 2024
68ebcb5
update
binsentsu Jul 17, 2024
f2b8181
update
binsentsu Jul 17, 2024
89d7dbe
update
binsentsu Jul 17, 2024
e54e8a2
update
binsentsu Jul 17, 2024
2c020b5
update
binsentsu Jul 17, 2024
50b9d76
update
binsentsu Jul 17, 2024
1612a6c
update
binsentsu Jul 18, 2024
537f1b6
update
binsentsu Jul 18, 2024
aa6a2ee
update
binsentsu Jul 18, 2024
3c160d8
update
binsentsu Jul 18, 2024
0449a7e
update
binsentsu Jul 18, 2024
b1216e8
update
binsentsu Jul 18, 2024
bb89453
update
binsentsu Jul 18, 2024
a18992b
update
binsentsu Jul 18, 2024
6512add
update
binsentsu Jul 18, 2024
ae01063
update
binsentsu Jul 18, 2024
e9bf01f
update
binsentsu Jul 18, 2024
8a26f20
update
binsentsu Jul 18, 2024
fd7afdf
update
binsentsu Jul 18, 2024
9e6db05
update
binsentsu Jul 18, 2024
d1e9e2d
update
binsentsu Jul 18, 2024
404a81d
use types for mqtt client (retain not correct)
binsentsu Aug 1, 2024
8898c0d
Retain Light status
binsentsu Nov 23, 2024
4844218
Retain the brightness
binsentsu Dec 11, 2024
e1d405c
Update dali2mqtt.py
binsentsu Dec 28, 2024
35e75f4
Update dali2mqtt.py
binsentsu Dec 28, 2024
0d23e91
Update dali2mqtt.py
binsentsu Dec 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM python:3.10.14-bookworm

WORKDIR /app

RUN apt-get update && apt-get install usbutils -y && pip install --upgrade pip

COPY . /app/dali2mqtt

RUN cd dali2mqtt && pip install -r requirements.txt

WORKDIR /app/dali2mqtt

ENTRYPOINT ["python", "-m", "dali2mqtt.dali2mqtt", "--config", "/app/dali-config/config.yaml"]
8 changes: 8 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
dali_driver: hasseb
devices_names: devices.yaml
ha_discovery_prefix: homeassistant
log_color: false
log_level: info
mqtt_base_topic: dali2mqtt
mqtt_port: 1883
mqtt_server: localhost
155 changes: 96 additions & 59 deletions dali2mqtt/dali2mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import logging
import random
import re
from threading import Thread
import time
import os
import concurrent.futures

import paho.mqtt.client as mqtt

Expand Down Expand Up @@ -66,6 +68,8 @@
logging.basicConfig(format=LOG_FORMAT, level=os.environ.get("LOGLEVEL", "INFO"))
logger = logging.getLogger(__name__)

associated_lamp_update_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)


def dali_scan(dali_driver):
"""Scan a maximum number of dali devices."""
Expand All @@ -92,10 +96,10 @@ def scan_groups(dali_driver, lamps):
try:
logging.debug("Search for groups for Lamp {}".format(lamp))
group1 = dali_driver.send(
gear.QueryGroupsZeroToSeven(address.Short(lamp))
gear.QueryGroupsZeroToSeven(lamp.short_address)
).value.as_integer
group2 = dali_driver.send(
gear.QueryGroupsEightToFifteen(address.Short(lamp))
gear.QueryGroupsEightToFifteen(lamp.short_address)
).value.as_integer

# logger.debug("Group 0-7: %d", group1)
Expand All @@ -117,15 +121,15 @@ def scan_groups(dali_driver, lamps):
groups[i + 8].append(lamp)
lamp_groups.append(i + 8)

logger.debug("Lamp %d is in groups %s", lamp, lamp_groups)
logger.debug("Lamp %d is in groups %s", lamp.short_address.address, lamp_groups)

except Exception as e:
logger.warning("Can't get groups for lamp %s: %s", lamp, e)
logger.info("Finished scanning for groups")
return groups


def initialize_lamps(data_object, client):
def initialize_lamps(data_object, client:mqtt.Client):
"""Initialize all lamps and groups."""

driver = data_object["driver"]
Expand All @@ -149,78 +153,88 @@ def create_mqtt_lamp(address, name):
address,
)

data_object["all_lamps"][name] = lamp_object
device_name = lamp_object.device_name
data_object["all_lamps"][device_name] = lamp_object

mqtt_data = [
(
HA_DISCOVERY_PREFIX.format(ha_prefix, name),
HA_DISCOVERY_PREFIX.format(ha_prefix, device_name),
lamp_object.gen_ha_config(mqtt_base_topic),
True,
),
(
MQTT_BRIGHTNESS_STATE_TOPIC.format(mqtt_base_topic, name),
MQTT_BRIGHTNESS_STATE_TOPIC.format(mqtt_base_topic, device_name),
lamp_object.level,
False,
True,
),
(
MQTT_BRIGHTNESS_MAX_LEVEL_TOPIC.format(mqtt_base_topic, name),
MQTT_BRIGHTNESS_MAX_LEVEL_TOPIC.format(mqtt_base_topic, device_name),
lamp_object.max_level,
True,
),
(
MQTT_BRIGHTNESS_MIN_LEVEL_TOPIC.format(mqtt_base_topic, name),
MQTT_BRIGHTNESS_MIN_LEVEL_TOPIC.format(mqtt_base_topic, device_name),
lamp_object.min_level,
True,
),
(
MQTT_BRIGHTNESS_PHYSICAL_MINIMUM_LEVEL_TOPIC.format(
mqtt_base_topic, name
mqtt_base_topic, device_name
),
lamp_object.min_physical_level,
True,
),
(
MQTT_STATE_TOPIC.format(mqtt_base_topic, name),
MQTT_STATE_TOPIC.format(mqtt_base_topic, device_name),
MQTT_PAYLOAD_ON if lamp_object.level > 0 else MQTT_PAYLOAD_OFF,
False,
True,
),
]
for topic, payload, retain in mqtt_data:
client.publish(topic, payload, retain)
client.publish(topic = topic, payload = payload, retain=retain)

logger.info(lamp_object)
return lamp_object

except DALIError as err:
logger.error("While initializing <%s> @ %s: %s", name, address, err)


lamp_objects = []
for lamp in lamps:
short_address = address.Short(lamp)

create_mqtt_lamp(
lamp_objects.append(create_mqtt_lamp(
short_address,
devices_names_config.get_friendly_name(short_address.address),
)
))

groups = scan_groups(driver, lamps)
for group in groups:
groups = scan_groups(driver, lamp_objects)
for group,lamps in groups.items():
logger.debug("Publishing group %d", group)

group_address = address.Group(int(group))
group_address = address.Group(int(group))
name = f"group_{group}"
name = devices_names_config.get_friendly_name(name)
group_object = create_mqtt_lamp(group_address, name)
"""Link all the lamps to the group object"""
group_object.associated_lamps = lamps
"""Link the group to the lamp"""
for lamp_object in lamps:
lamp_object.add_associated_lamp(group_object)

create_mqtt_lamp(group_address, f"group_{group}")

if devices_names_config.is_devices_file_empty():
devices_names_config.save_devices_names_file(data_object["all_lamps"])
logger.info("initialize_lamps finished")


def on_detect_changes_in_config(mqtt_client):
def on_detect_changes_in_config(mqtt_client:mqtt.Client):
"""Callback when changes are detected in the configuration file."""
logger.info("Reconnecting to server")
mqtt_client.disconnect()


def on_message_cmd(mqtt_client, data_object, msg):
def on_message_cmd(mqtt_client:mqtt.Client, data_object, msg):
"""Callback on MQTT command message."""
logger.debug("Command on %s: %s", msg.topic, msg.payload)
light = re.search(
Expand All @@ -236,33 +250,27 @@ def on_message_cmd(mqtt_client, data_object, msg):
MQTT_PAYLOAD_OFF,
retain=True,
)
update_associated_lamps(mqtt_client, data_object, lamp_object)
except DALIError as err:
logger.error("Failed to set light <%s> to OFF: %s", light, err)
except KeyError:
logger.error("Lamp %s doesn't exists", light)


def on_message_reinitialize_lamps_cmd(mqtt_client, data_object, msg):
def on_message_reinitialize_lamps_cmd(mqtt_client:mqtt.Client, data_object, msg):
"""Callback on MQTT scan lamps command message."""
logger.debug("Reinitialize Command on %s", msg.topic)
initialize_lamps(data_object, mqtt_client)


def get_lamp_object(data_object, light):
"""Retrieve lamp object from data object."""
if "group_" in light:
"""Check if the comand is for a dali group"""
group = int(re.search(r"group_(\d+)", light).group(1))
lamp_object = data_object["all_lamps"][group]
else:
"""The command is for a single lamp"""
if light not in data_object["all_lamps"]:
raise KeyError
lamp_object = data_object["all_lamps"][light]
return lamp_object


def on_message_brightness_cmd(mqtt_client, data_object, msg):
"""Retrieve lamp/group object from data object."""
if light not in data_object["all_lamps"]:
raise KeyError
return data_object["all_lamps"][light]


def on_message_brightness_cmd(mqtt_client:mqtt.Client, data_object, msg):
"""Callback on MQTT brightness command message."""
logger.debug("Brightness Command on %s: %s", msg.topic, msg.payload)
light = re.search(
Expand All @@ -273,22 +281,27 @@ def on_message_brightness_cmd(mqtt_client, data_object, msg):
lamp_object = get_lamp_object(data_object, light)

try:
lamp_object.level = int(msg.payload.decode("utf-8"))
new_level = int(msg.payload.decode("utf-8"))
if not lamp_object.level_change_needed(new_level):
return

lamp_object.level = new_level
if lamp_object.level == 0:
# 0 in DALI is turn off with fade out
lamp_object.off()
logger.debug("Set light <%s> to OFF", light)

mqtt_client.publish(
MQTT_STATE_TOPIC.format(data_object["base_topic"], light),
MQTT_PAYLOAD_ON if lamp_object.level != 0 else MQTT_PAYLOAD_OFF,
retain=False,
topic = MQTT_STATE_TOPIC.format(data_object["base_topic"], light),
payload = MQTT_PAYLOAD_ON if lamp_object.level != 0 else MQTT_PAYLOAD_OFF,
retain=True,
)
mqtt_client.publish(
MQTT_BRIGHTNESS_STATE_TOPIC.format(data_object["base_topic"], light),
lamp_object.level,
topic = MQTT_BRIGHTNESS_STATE_TOPIC.format(data_object["base_topic"], light),
payload = lamp_object.level,
retain=True,
)
update_associated_lamps(mqtt_client, data_object, lamp_object)
except ValueError as err:
logger.error(
"Can't convert <%s> to integer %d..%d: %s",
Expand All @@ -301,7 +314,7 @@ def on_message_brightness_cmd(mqtt_client, data_object, msg):
logger.error("Lamp %s doesn't exists", light)


def on_message_brightness_get_cmd(mqtt_client, data_object, msg):
def on_message_brightness_get_cmd(mqtt_client:mqtt.Client, data_object, msg):
"""Callback on MQTT brightness get command message."""
logger.debug("Brightness Get Command on %s: %s", msg.topic, msg.payload)
light = re.search(
Expand All @@ -310,21 +323,47 @@ def on_message_brightness_get_cmd(mqtt_client, data_object, msg):
).group(1)
try:
lamp_object = get_lamp_object(data_object, light)
retrieve_actual_level(mqtt_client, data_object, lamp_object)

except KeyError:
logger.error("Lamp %s doesn't exists", light)

def update_associated_lamps(mqtt_client:mqtt.Client, data_object, lamp_object):
if lamp_object.associated_lamps:
"""Give 1 sec to complete the main action before evaluating the associations"""
time.sleep(1)
associated_lamp_update_executor.submit(execute_update_associated_lamps, mqtt_client, data_object, lamp_object)

def execute_update_associated_lamps(mqtt_client:mqtt.Client, data_object, lamp_object):
if lamp_object.associated_lamps:
requested_lamps =[]
for assoc_lamp in lamp_object.associated_lamps:
if not assoc_lamp.device_name in requested_lamps:
retrieve_actual_level(mqtt_client, data_object, assoc_lamp)
requested_lamps.append(assoc_lamp.device_name)
if lamp_object.is_group():
if assoc_lamp.associated_lamps:
for nested_lamp in assoc_lamp.associated_lamps:
if nested_lamp.device_name != lamp_object.device_name and not nested_lamp.device_name in requested_lamps:
retrieve_actual_level(mqtt_client, data_object, nested_lamp)
requested_lamps.append(nested_lamp.device_name)

def retrieve_actual_level(mqtt_client:mqtt.Client, data_object, lamp_object):
try:
light = lamp_object.device_name
lamp_object.actual_level()
logger.debug("Get light <%s> results in %d", light, lamp_object.level)

mqtt_client.publish(
MQTT_BRIGHTNESS_STATE_TOPIC.format(data_object["base_topic"], light),
lamp_object.level,
retain=False,
topic= MQTT_BRIGHTNESS_STATE_TOPIC.format(data_object["base_topic"], light),
payload=lamp_object.level,
retain=True,
)

mqtt_client.publish(
MQTT_STATE_TOPIC.format(data_object["base_topic"], light),
MQTT_PAYLOAD_ON if lamp_object.level != 0 else MQTT_PAYLOAD_OFF,
retain=False,
topic = MQTT_STATE_TOPIC.format(data_object["base_topic"], light),
payload = MQTT_PAYLOAD_ON if lamp_object.level != 0 else MQTT_PAYLOAD_OFF,
retain=True,
)

except ValueError as err:
Expand All @@ -334,18 +373,16 @@ def on_message_brightness_get_cmd(mqtt_client, data_object, msg):
lamp_object.min_level,
lamp_object.max_level,
err,
)
except KeyError:
logger.error("Lamp %s doesn't exists", light)
)


def on_message(mqtt_client, data_object, msg): # pylint: disable=W0613
def on_message(mqtt_client:mqtt.Client, data_object, msg): # pylint: disable=W0613
"""Default callback on MQTT message."""
logger.error("Don't publish to %s", msg.topic)


def on_connect(
client,
client:mqtt.Client,
data_object,
flags,
result,
Expand All @@ -362,7 +399,7 @@ def on_connect(
]
)
client.publish(
MQTT_DALI2MQTT_STATUS.format(mqtt_base_topic), MQTT_AVAILABLE, retain=True
topic=MQTT_DALI2MQTT_STATUS.format(mqtt_base_topic), payload=MQTT_AVAILABLE, retain=True
)
initialize_lamps(data_object, client)

Expand All @@ -377,7 +414,7 @@ def create_mqtt_client(
devices_names_config,
ha_prefix,
log_level,
):
) -> mqtt.Client:
"""Create MQTT client object, setup callbacks and connection to server."""
logger.debug("Connecting to %s:%s", mqtt_server, mqtt_port)
mqttc = mqtt.Client(
Expand Down Expand Up @@ -422,7 +459,7 @@ def create_mqtt_client(

def main(args):
"""Main loop."""
mqttc = None
mqttc : mqtt.Client = None
config = Config(args, lambda: on_detect_changes_in_config(mqttc))

if config.log_color:
Expand Down
Loading