Python script to automatically generate configuration for ZiGate devices --may be inspiration for other "lots-of-boilerplate-configuration"

Hi! I have almost finished my script. Initially was a quick&dirty way of managing my Aqara devices, which I use through ZiGate platform. But the script and the main idea is quite clean and may be useful for some of you.

It’s quite rough and not documented, but at the end is doing simple things --a lot of simple things.

#!/usr/bin/env python3

import yaml
from enum import Enum


class SwitchType(Enum):
    SINGLE = "02"
    DOUBLE_LEFT = "02"
    DOUBLE_RIGHT = "03"


WALL_SWITCHES = [
    ("Headquarters", "8907", SwitchType.SINGLE),
    ("Terrace", "b4aa", SwitchType.SINGLE),
    ("Small bathroom", "2d6a", SwitchType.SINGLE),
    ("Main bedroom", "96f4", SwitchType.SINGLE),
    ("Main bathroom sink", "316a", SwitchType.DOUBLE_LEFT),
    ("Main bathroom ceiling", "316a", SwitchType.DOUBLE_RIGHT),
    ("Hall", "6f6c", SwitchType.SINGLE),
    ("Living room", "f9de", SwitchType.SINGLE),
    ("Kitchen ceiling", "7f9b", SwitchType.DOUBLE_LEFT),
    ("Kitchen countertop", "7f9b", SwitchType.DOUBLE_RIGHT),
    ("Laundry room", "711a", SwitchType.SINGLE),
    ("Guest room", "1879", SwitchType.SINGLE),
]

TEMPHUMID_SENSORS = [
    ("Terrace", "889a"),
    ("Headquarters", "c843"),
    ("Bedroom", "18d1"),
    ("Living room", "0279"),
]


def convert_to_entity(name, add_prefix=True):
    sanitized_name = name.replace(" ", "_").lower()
    if add_prefix:
        return "aqara_%s" % sanitized_name
    else:
        return sanitized_name


def build_wall_switches_config():
    result = list()

    for name, address, switch_type in WALL_SWITCHES:
        entry = {
            "platform": "zigate",
            "name": convert_to_entity(name),
            "address": "%s%s" % (address, switch_type.value),
            "default_state": "state",
            "inverted": "yes",
        }

        result.append(entry)
    return result


def build_lights_config():
    lights = dict()
    result = [{"platform": "template", "lights": lights}]

    for name, address, switch_type in WALL_SWITCHES:
        entity_name = convert_to_entity(name)
        entry = {
            "friendly_name": name,
            "entity_id": "switch.%s" % entity_name,
            "value_template": "{{ is_state('switch.%s', 'on') }}" % entity_name,
            "turn_on": {
                "service": "switch.turn_on",
                "data": {
                    "entity_id": "switch.%s" % entity_name,
                }
            },
            "turn_off": {
                "service": "switch.turn_off",
                "data": {
                    "entity_id": "switch.%s" % entity_name,
                }
            },
        }
        lights[convert_to_entity(name, add_prefix=False)] = entry

    return result


def build_temphumid_sensors_config():
    template_sensors = dict()
    result = [
        {
            "platform": "template",
            "sensors": template_sensors,
        }
    ]

    for name, address in TEMPHUMID_SENSORS:
        entity_name = convert_to_entity(name)
        sensor_entry = {
            "platform": "zigate",
            "name": entity_name,
            "address": "%s01" % address,
            # to ensure updates are correct, use the most mutable state as default
            "default_state": "temperature",
        }

        temp_entry = {
            "friendly_name": "%s temperature" % name,
            "entity_id": "sensor.%s" % entity_name,
            "unit_of_measurement": "°C",
            "value_template": "{{% if states.sensor.{0} %}}{{{{ "
                              "states.sensor.{0}.attributes.temperature "
                              "}}}}{{% else %}}??{{% endif %}}".format(entity_name),
        }

        humid_entry = {
            "friendly_name": "%s humidity" % name,
            "entity_id": "sensor.%s" % entity_name,
            "unit_of_measurement": "%",
            "value_template": "{{% if states.sensor.{0} %}}{{{{ "
                              "states.sensor.{0}.attributes.humidity "
                              "}}}}{{% else %}}??{{% endif %}}".format(entity_name),
        }

        batt_entry = {
            "friendly_name": "%s sensor battery" % name,
            "entity_id": "sensor.%s" % entity_name,
            "unit_of_measurement": "V",
            "value_template": "{{% if states.sensor.{0} %}}{{{{ "
                              "states.sensor.{0}.attributes.battery "
                              "}}}}{{% else %}}0{{% endif %}}".format(entity_name),
        }

        base_name = convert_to_entity(name, add_prefix=False)
        template_sensors["%s_humid" % base_name] = humid_entry
        template_sensors["%s_temp" % base_name] = temp_entry
        template_sensors["%s_batt" % base_name] = batt_entry
        result.append(sensor_entry)

    return result


def store_yaml(comment, content, path):
    with open(path, "w") as f:
        f.write("####################\n")
        f.write("# This YAML has been autogenerated by %s\n" % __file__)
        f.write("####################\n")
        f.write("\n")
        f.write("# %s\n" % comment)
        f.write("\n")
        yaml.dump(content, f, indent=2)


if __name__ == "__main__":
    store_yaml(
        "Aqara wall switches",
        build_wall_switches_config(),
        "./switches/aqara_wallswitches.yaml"
    )

    store_yaml(
        "Template lights based on Aqara wall switches",
        build_lights_config(),
        "./lights/aqara_lightswitches.yaml",
    )

    store_yaml(
        "Aqara temp+humid sensors",
        build_temphumid_sensors_config(),
        "./sensors/aqara_temp_humid.yaml"
    )

My workflow is:

  1. Change the script by adding some device or changing the way it creates the entities.
  2. Execute the script (locally).
  3. Check that there are no errors and the generated yaml files are well-formed.
  4. Push the changes.
  5. Go to the HASS server, pull changes.
  6. Restart HASS.

There are trivial improvements which I may end up doing, like:

  • Moving the WALL_SWITCHES configuration to an external file (probably YAML).
  • Adding some feedback information (a simple print saying “# sensors generated”).
  • Hooking the script into HASS itself and adding an interface button which would say “regenerate configuration and reload hass”.

For all this to work, the relevant parts of the configuration.yaml are the following

homeassistant:
  ...
  customize_glob:
    "*.aqara_*":
      hidden: true

sensor: !include_dir_merge_list sensors
switch: !include_dir_merge_list switches
light: !include_dir_merge_list lights

I hope it may be useful for somebody out there :slight_smile: