diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..1a7bfc7 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,37 @@ +name: Tests + +on: [push, pull_request] + +jobs: + + unit_tests: + name: Tests + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10'] + + steps: + # Checks out a copy of your repository on the ubuntu-latest machine + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest flake8 + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Lint with flake8 + run: | + flake8 dali2mqtt --count --show-source --statistics --max-line-length=127 + + - name: Test with pytest + timeout-minutes: 3 + run: | + pytest -vvv diff --git a/README.md b/README.md index dea9352..995d9af 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +![pytest workflow](https://github.com/dgomes/dali2mqtt/actions/workflows/pytest.yml/badge.svg) # dali2mqtt DALI <-> MQTT bridge @@ -35,7 +36,7 @@ pip install -r requirements.txt You can create a configuration file when you call the daemon the first time ```bash -venv/bin/python3 ./dali_mqtt_daemon.py +venv/bin/python3 -m dali2mqtt.dali2mqtt ``` Then just edit the file accordingly. You can also create the file with the right values, by using the arguments of dali_mqtt_daemon.py: diff --git a/dali2mqtt.service b/dali2mqtt.service index 94389aa..73d1557 100644 --- a/dali2mqtt.service +++ b/dali2mqtt.service @@ -3,8 +3,8 @@ Description=dali2mqtt After=network.target [Service] -ExecStart=/opt/dali2mqtt/venv/bin/python3 dali-mqtt-daemon.py -WorkingDirectory=/opt/dali2mqtt +ExecStart=/home/homeassistant/dali2mqtt/venv/bin/python3 -m dali2mqtt.dali2mqtt +WorkingDirectory=/home/homeassistant/dali2mqtt StandardOutput=inherit StandardError=inherit Environment=PYTHONUNBUFFERED=true diff --git a/dali2mqtt/__init__.py b/dali2mqtt/__init__.py new file mode 100644 index 0000000..b26da2a --- /dev/null +++ b/dali2mqtt/__init__.py @@ -0,0 +1 @@ +"""DALI 2 MQTT package.""" diff --git a/config.py b/dali2mqtt/config.py similarity index 96% rename from config.py rename to dali2mqtt/config.py index 1860fa5..dc593b8 100644 --- a/config.py +++ b/dali2mqtt/config.py @@ -1,35 +1,34 @@ """Configuration Object.""" import logging -import yaml -import voluptuous as vol -from watchdog.events import FileSystemEventHandler -from watchdog.observers.polling import PollingObserver as Observer -from consts import ( - DEFAULT_MQTT_PORT, - DEFAULT_MQTT_SERVER, - DEFAULT_HA_DISCOVERY_PREFIX, - DEFAULT_MQTT_BASE_TOPIC, - DEFAULT_DEVICES_NAMES_FILE, - DEFAULT_LOG_LEVEL, - DEFAULT_LOG_COLOR, - DEFAULT_DALI_DRIVER, - DALI_DRIVERS, +import voluptuous as vol +import yaml +from dali2mqtt.consts import ( ALL_SUPPORTED_LOG_LEVELS, - LOG_FORMAT, CONF_CONFIG, CONF_DALI_DRIVER, + CONF_DEVICES_NAMES_FILE, + CONF_HA_DISCOVERY_PREFIX, CONF_LOG_COLOR, CONF_LOG_LEVEL, - CONF_HA_DISCOVERY_PREFIX, - CONF_DEVICES_NAMES_FILE, CONF_MQTT_BASE_TOPIC, + CONF_MQTT_PASSWORD, CONF_MQTT_PORT, CONF_MQTT_SERVER, CONF_MQTT_USERNAME, - CONF_MQTT_PASSWORD, + DALI_DRIVERS, + DEFAULT_DALI_DRIVER, + DEFAULT_DEVICES_NAMES_FILE, + DEFAULT_HA_DISCOVERY_PREFIX, + DEFAULT_LOG_COLOR, + DEFAULT_LOG_LEVEL, + DEFAULT_MQTT_BASE_TOPIC, + DEFAULT_MQTT_PORT, + DEFAULT_MQTT_SERVER, + LOG_FORMAT, ) - +from watchdog.events import FileSystemEventHandler +from watchdog.observers.polling import PollingObserver as Observer CONF_SCHEMA = vol.Schema( { @@ -101,7 +100,8 @@ def load_config_file(self): ) configuration = {} self._config = CONF_SCHEMA(configuration) - self._callback() + if self._callback: + self._callback() except AttributeError: # No callback configured pass @@ -112,8 +112,8 @@ def load_config_file(self): def save_config_file(self): """Save configuration back to yaml file.""" try: + cfg = self._config.pop(CONF_CONFIG) # temporary displace config file with open(self._path, "w", encoding="utf8") as outfile: - cfg = self._config.pop(CONF_CONFIG) # temporary displace config file yaml.dump( self._config, outfile, default_flow_style=False, allow_unicode=True ) diff --git a/consts.py b/dali2mqtt/consts.py similarity index 97% rename from consts.py rename to dali2mqtt/consts.py index 04b88b1..f662ffb 100644 --- a/consts.py +++ b/dali2mqtt/consts.py @@ -1,8 +1,9 @@ +"""Constants common the various modules.""" + import logging -"""Constants common the various modules.""" __author__ = "Diogo Gomes" -__version__ = "0.0.1" +__version__ = "0.0.3" __email__ = "diogogomes@gmail.com" HASSEB = "hasseb" @@ -52,6 +53,7 @@ MIN_HASSEB_FIRMWARE_VERSION = 2.3 MIN_BACKOFF_TIME = 2 +MAX_BACKOFF_TIME = 10 MAX_RETRIES = 10 ALL_SUPPORTED_LOG_LEVELS = { diff --git a/dali_mqtt_daemon.py b/dali2mqtt/dali2mqtt.py similarity index 66% rename from dali_mqtt_daemon.py rename to dali2mqtt/dali2mqtt.py index 96bb3b3..e7904a5 100644 --- a/dali_mqtt_daemon.py +++ b/dali2mqtt/dali2mqtt.py @@ -6,11 +6,19 @@ import random import re import time +import os + +import paho.mqtt.client as mqtt import dali.address as address import dali.gear.general as gear -import paho.mqtt.client as mqtt -from consts import ( +from dali.command import YesNoResponse +from dali.exceptions import DALIError + +from dali2mqtt.devicesnamesconfig import DevicesNamesConfig +from dali2mqtt.lamp import Lamp +from dali2mqtt.config import Config +from dali2mqtt.consts import ( ALL_SUPPORTED_LOG_LEVELS, CONF_CONFIG, CONF_DALI_DRIVER, @@ -33,6 +41,7 @@ LOG_FORMAT, MAX_RETRIES, MIN_BACKOFF_TIME, + MAX_BACKOFF_TIME, MIN_HASSEB_FIRMWARE_VERSION, MQTT_AVAILABLE, MQTT_BRIGHTNESS_COMMAND_TOPIC, @@ -52,24 +61,21 @@ TRIDONIC, YELLOW_COLOR, ) -from dali.command import YesNoResponse -from dali.exceptions import DALIError -from devicesnamesconfig import DevicesNamesConfig -from lamp import Lamp -from config import Config -logging.basicConfig(format=LOG_FORMAT) +logging.basicConfig(format=LOG_FORMAT, level=os.environ.get("LOGLEVEL", "INFO")) logger = logging.getLogger(__name__) -def dali_scan(driver): +def dali_scan(dali_driver): """Scan a maximum number of dali devices.""" lamps = [] for lamp in range(0, 63): try: logging.debug("Search for Lamp %s", lamp) - present = driver.send(gear.QueryControlGearPresent(address.Short(lamp))) + present = dali_driver.send( + gear.QueryControlGearPresent(address.Short(lamp)) + ) if isinstance(present, YesNoResponse) and present.value: lamps.append(lamp) logger.debug("Found lamp at address %d", lamp) @@ -122,165 +128,86 @@ def scan_groups(dali_driver, lamps): def initialize_lamps(data_object, client): """Initialize all lamps and groups.""" - driver_object = data_object["driver"] + driver = data_object["driver"] mqtt_base_topic = data_object["base_topic"] ha_prefix = data_object["ha_prefix"] log_level = data_object["log_level"] devices_names_config = data_object["devices_names_config"] devices_names_config.load_devices_names_file() - lamps = dali_scan(driver_object) + lamps = dali_scan(driver) logger.info( "Found %d lamps", len(lamps), ) - for lamp in lamps: + def create_mqtt_lamp(address, name): try: - short_address = address.Short(lamp) - actual_level = driver_object.send(gear.QueryActualLevel(short_address)) - physical_minimum = driver_object.send( - gear.QueryPhysicalMinimum(short_address) - ) - min_level = driver_object.send(gear.QueryMinLevel(short_address)) - max_level = driver_object.send(gear.QueryMaxLevel(short_address)) - device_name = devices_names_config.get_friendly_name(short_address.address) - lamp = device_name - lamp_object = Lamp( log_level, - driver_object, - device_name, - short_address, - physical_minimum.value, - min_level.value, - actual_level.value, - max_level.value, + driver, + name, + address, ) - data_object["all_lamps"][lamp_object.device_name] = lamp_object - lamp = lamp_object.device_name + data_object["all_lamps"][name] = lamp_object - client.publish( - HA_DISCOVERY_PREFIX.format(ha_prefix, lamp), - lamp_object.gen_ha_config(mqtt_base_topic), - retain=True, - ) - client.publish( - MQTT_BRIGHTNESS_STATE_TOPIC.format(mqtt_base_topic, lamp), - actual_level.value, - retain=False, - ) - - client.publish( - MQTT_BRIGHTNESS_MAX_LEVEL_TOPIC.format(mqtt_base_topic, lamp), - max_level.value, - retain=True, - ) - client.publish( - MQTT_BRIGHTNESS_MIN_LEVEL_TOPIC.format(mqtt_base_topic, lamp), - min_level.value, - retain=True, - ) - client.publish( - MQTT_BRIGHTNESS_PHYSICAL_MINIMUM_LEVEL_TOPIC.format( - mqtt_base_topic, lamp + mqtt_data = [ + ( + HA_DISCOVERY_PREFIX.format(ha_prefix, name), + lamp_object.gen_ha_config(mqtt_base_topic), + True, ), - physical_minimum.value, - retain=True, - ) - client.publish( - MQTT_STATE_TOPIC.format(mqtt_base_topic, lamp), - MQTT_PAYLOAD_ON if actual_level.value > 0 else MQTT_PAYLOAD_OFF, - retain=False, - ) - logger.info( - " - short address: %d, actual brightness level: %d (minimum: %d, max: %d, physical minimum: %d)", - short_address.address, - actual_level.value, - min_level.value, - max_level.value, - physical_minimum.value, - ) - - except DALIError as err: - logger.error("While initializing lamp<%s>: %s", lamp, err) + ( + MQTT_BRIGHTNESS_STATE_TOPIC.format(mqtt_base_topic, name), + lamp_object.level, + False, + ), + ( + MQTT_BRIGHTNESS_MAX_LEVEL_TOPIC.format(mqtt_base_topic, name), + lamp_object.max_level, + True, + ), + ( + MQTT_BRIGHTNESS_MIN_LEVEL_TOPIC.format(mqtt_base_topic, name), + lamp_object.min_level, + True, + ), + ( + MQTT_BRIGHTNESS_PHYSICAL_MINIMUM_LEVEL_TOPIC.format( + mqtt_base_topic, name + ), + lamp_object.min_physical_level, + True, + ), + ( + MQTT_STATE_TOPIC.format(mqtt_base_topic, name), + MQTT_PAYLOAD_ON if lamp_object.level > 0 else MQTT_PAYLOAD_OFF, + False, + ), + ] + for topic, payload, retain in mqtt_data: + client.publish(topic, payload, retain) - groups = scan_groups(driver_object, lamps) - for group in groups: - logger.debug("Publishing group %d", group) - try: - logger.debug("Group %s" % group) - group_address = address.Group(int(group)) + logger.info(lamp_object) - actual_level = driver_object.send(gear.QueryActualLevel(group_address)) - physical_minimum = driver_object.send( - gear.QueryPhysicalMinimum(group_address) - ) - min_level = driver_object.send(gear.QueryMinLevel(group_address)) - max_level = driver_object.send(gear.QueryMaxLevel(group_address)) - device_name = f"group_{group}" - - group_lamp = device_name - logger.debug("Group Name: %s", group_lamp) + except DALIError as err: + logger.error("While initializing <%s> @ %s: %s", name, address, err) - lamp_object = Lamp( - log_level, - driver_object, - device_name, - group_address, - physical_minimum.value, - min_level.value, - actual_level.value, - max_level.value, - ) + for lamp in lamps: + short_address = address.Short(lamp) - data_object["all_lamps"][lamp_object.device_name] = lamp_object - group_lamp = lamp_object.device_name + create_mqtt_lamp( + short_address, + devices_names_config.get_friendly_name(short_address.address), + ) - client.publish( - HA_DISCOVERY_PREFIX.format(ha_prefix, group_lamp), - lamp_object.gen_ha_config(mqtt_base_topic), - retain=True, - ) - client.publish( - MQTT_BRIGHTNESS_STATE_TOPIC.format(mqtt_base_topic, group_lamp), - actual_level.value, - retain=False, - ) + groups = scan_groups(driver, lamps) + for group in groups: + logger.debug("Publishing group %d", group) - client.publish( - MQTT_BRIGHTNESS_MAX_LEVEL_TOPIC.format(mqtt_base_topic, group_lamp), - max_level.value, - retain=True, - ) - client.publish( - MQTT_BRIGHTNESS_MIN_LEVEL_TOPIC.format(mqtt_base_topic, group_lamp), - min_level.value, - retain=True, - ) - client.publish( - MQTT_BRIGHTNESS_PHYSICAL_MINIMUM_LEVEL_TOPIC.format( - mqtt_base_topic, group_lamp - ), - physical_minimum.value, - retain=True, - ) - client.publish( - MQTT_STATE_TOPIC.format(mqtt_base_topic, group_lamp), - MQTT_PAYLOAD_ON if actual_level.value > 0 else MQTT_PAYLOAD_OFF, - retain=False, - ) - logger.info( - " - group address: %s, actual brightness level: %d (minimum: %d, max: %d, physical minimum: %d)", - group_address.group, - actual_level.value, - min_level.value, - max_level.value, - physical_minimum.value, - ) + group_address = address.Group(int(group)) - except DALIError as err: - logger.error("Error while initializing group <%s>: %s", group_lamp, err) + 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"]) @@ -303,20 +230,20 @@ def on_message_cmd(mqtt_client, data_object, msg): try: lamp_object = data_object["all_lamps"][light] logger.debug("Set light <%s> to %s", light, msg.payload) - data_object["driver"].send(gear.Off(lamp_object.short_address)) + lamp_object.off() mqtt_client.publish( MQTT_STATE_TOPIC.format(data_object["base_topic"], light), MQTT_PAYLOAD_OFF, retain=True, ) except DALIError as err: - logger.error("Failed to set light <%s> to %s: %s", light, "OFF", 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): - """Callback on MQTT scan lamps command message""" + """Callback on MQTT scan lamps command message.""" logger.debug("Reinitialize Command on %s", msg.topic) initialize_lamps(data_object, mqtt_client) @@ -325,7 +252,7 @@ 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("group_(\d+)", light).group(1)) + 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""" @@ -345,14 +272,11 @@ def on_message_brightness_cmd(mqtt_client, data_object, msg): try: lamp_object = get_lamp_object(data_object, light) - level = None try: - level = msg.payload.decode("utf-8") - level = int(level) - lamp_object.level = level + lamp_object.level = int(msg.payload.decode("utf-8")) if lamp_object.level == 0: # 0 in DALI is turn off with fade out - data_object["driver"].send(gear.Off(lamp_object.short_address)) + lamp_object.off() logger.debug("Set light <%s> to OFF", light) mqtt_client.publish( @@ -368,7 +292,7 @@ def on_message_brightness_cmd(mqtt_client, data_object, msg): except ValueError as err: logger.error( "Can't convert <%s> to integer %d..%d: %s", - str(level), + msg.payload.decode("utf-8"), lamp_object.min_level, lamp_object.max_level, err, @@ -388,27 +312,25 @@ def on_message_brightness_get_cmd(mqtt_client, data_object, msg): lamp_object = get_lamp_object(data_object, light) try: - level = data_object["driver"].send( - gear.QueryActualLevel(lamp_object.short_address) - ) - logger.debug("Get light <%s> results in %d", light, level.value) + 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), - level.value, + lamp_object.level, retain=False, ) mqtt_client.publish( MQTT_STATE_TOPIC.format(data_object["base_topic"], light), - MQTT_PAYLOAD_ON if level.value != 0 else MQTT_PAYLOAD_OFF, + MQTT_PAYLOAD_ON if lamp_object.level != 0 else MQTT_PAYLOAD_OFF, retain=False, ) except ValueError as err: logger.error( "Can't convert <%s> to integer %d..%d: %s", - str(level), + lamp_object.level, lamp_object.min_level, lamp_object.max_level, err, @@ -446,7 +368,7 @@ def on_connect( def create_mqtt_client( - driver_object, + driver, mqtt_server, mqtt_port, mqtt_username, @@ -461,7 +383,7 @@ def create_mqtt_client( mqttc = mqtt.Client( client_id="dali2mqtt", userdata={ - "driver": driver_object, + "driver": driver, "base_topic": mqtt_base_topic, "ha_prefix": ha_prefix, "devices_names_config": devices_names_config, @@ -498,12 +420,8 @@ def create_mqtt_client( return mqttc -def delay(): - """Generate a random backoff time.""" - return MIN_BACKOFF_TIME + random.randint(0, 1000) / 1000.0 - - def main(args): + """Main loop.""" mqttc = None config = Config(args, lambda: on_detect_changes_in_config(mqttc)) @@ -545,23 +463,26 @@ def main(args): dali_driver = DaliServer("localhost", 55825) - should_backoff = True retries = 0 - run = True - while run: - mqttc = create_mqtt_client( - dali_driver, - *config.mqtt_conf, - devices_names_config, - config.ha_discovery_prefix, - config.log_level, - ) - mqttc.loop_forever() - if should_backoff: - if retries == MAX_RETRIES: - run = False - time.sleep(delay()) - retries += 1 # TODO reset on successfull connection + while retries < MAX_RETRIES: + try: + mqttc = create_mqtt_client( + dali_driver, + *config.mqtt_conf, + devices_names_config, + config.ha_discovery_prefix, + config.log_level, + ) + mqttc.loop_forever() + retries = ( + 0 # if we reach here, it means we where already connected successfully + ) + except Exception as e: + logger.error("%s: %s", type(e).__name__, e) + time.sleep(random.randint(MIN_BACKOFF_TIME, MAX_BACKOFF_TIME)) + retries += 1 + + logger.error("Maximum retries of %d reached, exiting...", retries) if __name__ == "__main__": diff --git a/devicesnamesconfig.py b/dali2mqtt/devicesnamesconfig.py similarity index 89% rename from devicesnamesconfig.py rename to dali2mqtt/devicesnamesconfig.py index a8bea73..4b861f2 100644 --- a/devicesnamesconfig.py +++ b/dali2mqtt/devicesnamesconfig.py @@ -2,7 +2,7 @@ import logging import yaml -from consts import ALL_SUPPORTED_LOG_LEVELS, LOG_FORMAT +from dali2mqtt.consts import ALL_SUPPORTED_LOG_LEVELS, LOG_FORMAT logging.basicConfig(format=LOG_FORMAT) logger = logging.getLogger(__name__) @@ -40,8 +40,11 @@ def load_devices_names_file(self): except yaml.YAMLError as error: logger.error("In devices file %s: %s", self._path, error) raise DevicesNamesConfigLoadError() - except Exception as err: - logger.error("Could not load device names config <%s>, a new one will be created after successfull start", self._path) + except Exception: + logger.error( + "Could not load device names config <%s>, a new one will be created after successfull start", + self._path, + ) def save_devices_names_file(self, all_lamps): """Save configuration back to yaml file.""" diff --git a/lamp.py b/dali2mqtt/lamp.py similarity index 68% rename from lamp.py rename to dali2mqtt/lamp.py index 283e4f6..272948d 100644 --- a/lamp.py +++ b/dali2mqtt/lamp.py @@ -3,7 +3,7 @@ import logging import dali.gear.general as gear -from consts import ( +from dali2mqtt.consts import ( ALL_SUPPORTED_LOG_LEVELS, LOG_FORMAT, MQTT_AVAILABLE, @@ -14,8 +14,6 @@ MQTT_NOT_AVAILABLE, MQTT_PAYLOAD_OFF, MQTT_STATE_TOPIC, - __author__, - __email__, __version__, ) from slugify import slugify @@ -33,20 +31,21 @@ def __init__( driver, friendly_name, short_address, - min_physical_level, - min_level, - level, - max_level, ): """Initialize Lamp.""" self.driver = driver self.short_address = short_address self.friendly_name = friendly_name + self.device_name = slugify(friendly_name) - self.min_physical_level = min_physical_level - self.min_level = min_level - self.max_level = max_level - self.level = level + + self.min_physical_level = driver.send( + gear.QueryPhysicalMinimum(short_address) + ).value + self.min_level = driver.send(gear.QueryMinLevel(short_address)).value + self.max_level = driver.send(gear.QueryMaxLevel(short_address)).value + self.level = driver.send(gear.QueryActualLevel(short_address)).value + logger.setLevel(ALL_SUPPORTED_LOG_LEVELS[log_level]) def gen_ha_config(self, mqtt_base_topic): @@ -56,10 +55,8 @@ def gen_ha_config(self, mqtt_base_topic): "obj_id": f"dali_light_{self.device_name}", "uniq_id": f"{type(self.driver).__name__}_{self.short_address}", "stat_t": MQTT_STATE_TOPIC.format(mqtt_base_topic, self.device_name), - "cmd_t": MQTT_COMMAND_TOPIC.format( - mqtt_base_topic, self.device_name - ), - "pl_off": MQTT_PAYLOAD_OFF.decode('utf-8'), + "cmd_t": MQTT_COMMAND_TOPIC.format(mqtt_base_topic, self.device_name), + "pl_off": MQTT_PAYLOAD_OFF.decode("utf-8"), "bri_stat_t": MQTT_BRIGHTNESS_STATE_TOPIC.format( mqtt_base_topic, self.device_name ), @@ -81,6 +78,10 @@ def gen_ha_config(self, mqtt_base_topic): } return json.dumps(json_config) + def actual_level(self): + """Retrieve actual level from ballast.""" + self.__level = self.driver.send(gear.QueryActualLevel(self.short_address)) + @property def level(self): """Return brightness level.""" @@ -88,6 +89,7 @@ def level(self): @level.setter def level(self, value): + """Commit level to ballast.""" if not self.min_level <= value <= self.max_level and value != 0: raise ValueError self.__level = value @@ -95,3 +97,15 @@ def level(self, value): logger.debug( "Set lamp <%s> brightness level to %s", self.friendly_name, self.level ) + + def off(self): + """Turn off ballast.""" + self.driver.send(gear.Off(self.short_address)) + + def __str__(self): + """Serialize lamp information.""" + return ( + f"{self.device_name} - address: {self.short_address.address}, " + f"actual brightness level: {self.level} (minimum: {self.min_level}, " + f"max: {self.max_level}, physical minimum: {self.min_physical_level})" + ) diff --git a/tests/__init__.py b/tests/__init__.py index 7b49369..517887d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,7 +1 @@ -import os -import sys -PROJECT_PATH = os.getcwd() -SOURCE_PATH = os.path.join( - PROJECT_PATH,"." -) -sys.path.append(SOURCE_PATH) +"""Tests for dali2mqtt.""" diff --git a/tests/data/config.yaml b/tests/data/config.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..8d9ee62 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,17 @@ +"""Tests for config.""" + +from dali2mqtt.config import Config +from unittest import mock + +def test_load_config(): + args = mock.Mock() + args.config = "tests/data/config.yaml" + + cfg = Config(args) + + assert cfg.mqtt_conf == ('localhost', 1883, None, None, 'dali2mqtt') + assert cfg,dali_driver == "hasseb" + assert cfg.ha_discovery_prefix == "homeassistant" + assert cfg.log_level == "info" + assert cfg.log_color == False + assert cfg.devices_names_file == "devices.yaml" \ No newline at end of file diff --git a/tests/test_hasseb.py b/tests/test_hasseb.py index 9865cec..f59cfc2 100644 --- a/tests/test_hasseb.py +++ b/tests/test_hasseb.py @@ -1,18 +1,20 @@ """Tests based on hasseb driver.""" -from dali_mqtt_daemon import main +from dali2mqtt.dali2mqtt import main from unittest import mock import pytest -from consts import ( +from dali2mqtt.consts import ( DEFAULT_CONFIG_FILE, DEFAULT_MQTT_SERVER, DEFAULT_MQTT_PORT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_COLOR, DEFAULT_HA_DISCOVERY_PREFIX, + MAX_RETRIES, ) + @pytest.fixture def args(): mock_args = mock.Mock() @@ -25,24 +27,51 @@ def args(): mock_args.log_color = DEFAULT_LOG_COLOR return mock_args + @pytest.fixture def config(): - return {"config": "config.yaml", - "dali_driver": "hasseb", - "dali_lamps": 2, - "mqtt_server": "localhost", - "mqtt_port": 1883, - "mqtt_base_topic": "dali2mqtt", - "ha_discovery_prefix": "homeassistant", - "log_level": "info", - "log_color": False, + return { + "config": "config.yaml", + "dali_driver": "hasseb", + "dali_lamps": 2, + "mqtt_server": "localhost", + "mqtt_port": 1883, + "mqtt_base_topic": "dali2mqtt", + "ha_discovery_prefix": "homeassistant", + "log_level": "info", + "log_color": False, } -def test_main(args, config): - with mock.patch('dali_mqtt_daemon.create_mqtt_client', return_value=mock.Mock()) as mock_mqtt_client: - with mock.patch("dali_mqtt_daemon.delay", return_value=0): - with mock.patch('yaml.load', return_value={}) as mock_config_file: - mock_mqtt_client.loop_forever = mock.Mock() - main(args) - mock_config_file.assert_called() - assert mock_mqtt_client.call_count == 11 \ No newline at end of file +@pytest.fixture +def fake_data_object(): + driver = mock.Mock() + driver.send = lambda x: 0x00 + + return { + "driver": driver, + "base_topic": "test", + "ha_prefix": "hass", + "log_level": "debug", + "devices_names_config": None + } + +@pytest.fixture +def fake_mqttc(): + mqttc = mock.Mock() + def loop_forever(): + import sys + raise Exception() + mqttc.loop_forever = loop_forever + return mqttc + + +def test_main(args, config, fake_mqttc, caplog): + """Test main loop.""" + with mock.patch( + "dali2mqtt.dali2mqtt.create_mqtt_client", return_value=fake_mqttc + ) as mock_mqtt_client: + with mock.patch("time.sleep", return_value=None) as sleep: + main(args) + assert sleep.call_count == MAX_RETRIES + assert mock_mqtt_client.call_count == MAX_RETRIES + assert any("Maximum retries of 10 reached, exiting" in rec.message for rec in caplog.records) diff --git a/tests/test_lamp.py b/tests/test_lamp.py new file mode 100644 index 0000000..a9375e5 --- /dev/null +++ b/tests/test_lamp.py @@ -0,0 +1,76 @@ +"""Tests for lamp.""" + +from dali2mqtt.lamp import Lamp +from dali2mqtt.consts import __version__ +from unittest import mock +from dali.address import Short +import pytest +import json +from slugify import slugify + +MIN__PHYSICAL_BRIGHTNESS = 1 +MIN_BRIGHTNESS = 2 +MAX_BRIGHTNESS = 250 +ACTUAL_BRIGHTNESS = 100 + +def generate_driver_values(results): + for res in results: + result = mock.Mock() + result.value = res + print(result.value) + yield result + +@pytest.fixture +def fake_driver(): + drive = mock.Mock() + drive.dummy = generate_driver_values([MIN__PHYSICAL_BRIGHTNESS, MIN_BRIGHTNESS, MAX_BRIGHTNESS, ACTUAL_BRIGHTNESS, ACTUAL_BRIGHTNESS]) + drive.send = lambda x: next(drive.dummy) + return drive + + +@pytest.fixture +def fake_address(): + address = mock.Mock() + address.address = 1 + address.__repr__ = lambda: "1" + +def test_ha_config(fake_driver, fake_address): + + friendly_name = "my lamp" + addr_number = 1 + addr = Short(1) + + lamp1 = Lamp( + log_level="debug", + driver=fake_driver, + friendly_name=friendly_name, + short_address=addr, + ) + + assert lamp1.device_name == slugify(friendly_name) + assert lamp1.short_address.address == addr_number + + assert str(lamp1) == f'my-lamp - address: {addr_number}, actual brightness level: {ACTUAL_BRIGHTNESS} (minimum: {MIN_BRIGHTNESS}, max: {MAX_BRIGHTNESS}, physical minimum: {MIN__PHYSICAL_BRIGHTNESS})' + + assert json.loads(lamp1.gen_ha_config("test")) == { + "name": friendly_name, + "obj_id": "dali_light_my-lamp", + "uniq_id": f"Mock_{addr}", + "stat_t": "test/my-lamp/light/status", + "cmd_t": "test/my-lamp/light/switch", + "pl_off": "OFF", + "bri_stat_t": "test/my-lamp/light/brightness/status", + "bri_cmd_t": "test/my-lamp/light/brightness/set", + "bri_scl": MAX_BRIGHTNESS, + "on_cmd_type": "brightness", + "avty_t": "test/status", + "pl_avail": "online", + "pl_not_avail": "offline", + "device": { + "ids": "dali2mqtt", + "name": "DALI Lights", + "sw": f"dali2mqtt {__version__}", + "mdl": "Mock", + "mf": "dali2mqtt", + }, + }