diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8847bc8e..c41ca078 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.1.1 +current_version = 1.2.0 commit = True tag = False diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..1dc4f3d8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[html] +show_contexts = True diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e161b8f..426bc67b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Revision Change History +## [1.2.0] + +### Fixes + +- Fix error description #488 +- Allow scenes.yaml file to be empty Thanks @kpfleming #497 +- Fixes for HomeAssitant changes #505 +- Suppress runtime error in Web UI #515 +- Change default Friendly Name of devices in Home Assitant Discovery to comply with new standards. If you have defined your own templates, you may need to adjust the name field. See ([HomeAssistant Entity Naming Guidelines](https://developers.home-assistant.io/docs/core/entity/#entity-naming)) ([Change in MQTT Naming](https://github.com/home-assistant/core/pull/95159)) #516 +- Fix Bug cuasing switches turned off to be reported as on. Thanks @tstabrawa #517 +- Allow for use of mode strings "instant", "fast", "normal" ... as described in the documentation. #519 +- Reverse dusk and dawn signal. #520 +- Prevent Refresh Commands from piling up on battery devices. Thanks @tstabrawa #521 + +### Additions + +- New improvements to the Home Assistant Discovery Platform system. Now allows for overriding values for an individual device. Thanks @kpfleming ([Documentation](https://github.com/TD22057/insteon-mqtt/blob/master/docs/discovery_customization_config.md)) + ## [1.1.1] ### Fixes diff --git a/README.md b/README.md index a76e1d5a..f4c9b4ef 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,12 @@ integrated into and controlled from anything that can use MQTT. This package works well with HomeAssistant and can be easily [installed as an addon](docs/HA_Addon_Instructions.md) using the HomeAssistant Supervisor. -Version: 1.1.1 ([History](CHANGELOG.md)) +Version: 1.2.0 ([History](CHANGELOG.md)) ### Recent Breaking Changes - 0.9.1 - A Yaml validation routine was added. If you have an error in your config.yaml file, you will get an error on startup. -- 0.8.3 - HomeAssistant version 2021.4.0 now only supports percentages for fan - speeds. This means any fan entities in HomeAssistant that were configured - to use "low", "medium", and "high" for the fan speed will no longer work. - See [config-example.yaml](https://github.com/TD22057/insteon-mqtt/blob/master/config-example.yaml) - under the `mqtt -> fan` section for a suggest configuration in - HomeAssistant. __Users not using HomeAssistant are unaffected.__ -- 0.7.4 - IOLinc, the scene_topic has been elimited, please see the documentation - for the replaces functionality. -- 0.7.2 - KeypadLinc now supports both dimmer and on/off device types. This required - changing the KeypadLinc inputs in the MQTT portion of the config.yaml file. - See the file in the repository for the new input fields. ([Issue #33][I33]). - # Quickstart @@ -39,6 +27,9 @@ Using Home Assistant Supervisor? - [Installation Guide](docs/quick_start.md) - Install from the command line - [Startup Script](docs/auto_start.md) - Running InsteonMQTT on startup. - [Install as a HomeAssistant Addon](docs/HA_Addon_Instructions.md) - Install in HomeAssistant + - [Discovery for HomeAssistant](docs/discovery.md) - Automatic entity discovery + - [Customizing HomeAssistant Entities](docs/discovery_customization_ui.md) - Customizing entities in HomeAssistant + - [Discovery Overrides](docs/discovery_customization_config.md) - Alter discovery entities using templates and overrides - [Configuration](docs/configuration.md) - The base configuration requirements - [Initialize your Devices](docs/initializing.md) - Setting up your Insteon Devices - [User Interface Options](docs/user_interface.md) - Three available user interfaces. @@ -121,3 +112,17 @@ Thanks to [Insteon terminal](https://github.com/pfrommerd/insteon-terminal), without the work that went into that repo, it would have taken me forever to get this to work. I learned all of the command protocols and database managemenet commands from inspecting that code. + +# Old Breaking Changes + +- 0.8.3 - HomeAssistant version 2021.4.0 now only supports percentages for fan + speeds. This means any fan entities in HomeAssistant that were configured + to use "low", "medium", and "high" for the fan speed will no longer work. + See [config-example.yaml](https://github.com/TD22057/insteon-mqtt/blob/master/config-example.yaml) + under the `mqtt -> fan` section for a suggest configuration in + HomeAssistant. __Users not using HomeAssistant are unaffected.__ +- 0.7.4 - IOLinc, the scene_topic has been elimited, please see the documentation + for the replaces functionality. +- 0.7.2 - KeypadLinc now supports both dimmer and on/off device types. This required + changing the KeypadLinc inputs in the MQTT portion of the config.yaml file. + See the file in the repository for the new input fields. ([Issue #33][I33]). diff --git a/config.json b/config.json index 053b687b..90be78e0 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "name": "Insteon MQTT", "description": "Creates an MQTT interface to the Insteon protocol.", "slug": "insteon-mqtt", - "version": "1.1.1", + "version": "1.2.0", "startup": "services", "arch": ["amd64","armhf","aarch64","i386"], "boot": "auto", @@ -12,5 +12,6 @@ "schema": {}, "image": "td22057/{arch}-insteon-mqtt", "ingress": true, - "ingress_port": 8099 + "ingress_port": 8099, + "init": false } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6e624991..4159bca1 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -61,8 +61,8 @@ added. # Show lines that need coverage pytest --cov=insteon_mqtt --cov-report term-missing - # Create html files that show missing lines - pytest --cov=insteon_mqtt --cov-report html + # Create html files that show missing lines and which tests are run on which lines + pytest --cov=insteon_mqtt --cov-report=html --cov-context=test ``` # Logging diff --git a/docs/config_extra.md b/docs/config_extra.md index 12fce754..a45139d0 100644 --- a/docs/config_extra.md +++ b/docs/config_extra.md @@ -26,7 +26,15 @@ This key can be used to define a custom discovery template for this device. The value of this setting should be a subkey under the `mqtt` key in the yaml config file. This subkey must contain a `discovery_entities` list of each of the discovery entities. For more details and examples see -[Discovery](https://github.com/TD22057/insteon-mqtt/blob/master/docs/discovery.md). +[Discovery](discovery.md). + +### `discovery_override_class` - Discovery Override Class +Details of this setting can be found in [Discovery Customization in +Insteon-MQTT](discovery_customizaton_config.md). + +### `discoverable` - Discovery Control +Details of this setting can be found in [Discovery Customization in +Insteon-MQTT](discovery_customizaton_config.md). ## Advanced Settings These settings can be used with all device types, but should be considered advanced. These settings may cause undesirable effects. diff --git a/docs/configuration.md b/docs/configuration.md index 5dd7cc29..b1de2b96 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,7 +17,7 @@ The following is the bare minimum of changes to the `config.yaml` file that are Starting in version 1.0.0 InsteonMQTT now includes a base configuration file which the user configuration file is overlayed over the top. You should not directly edit the base configuration file, as it will be overwritten during updates. If you want to change a default setting, simply define the same key in your user configuration file and set your desired value. You can view the contents of the base configuration file here: -[config-base.yaml](https://github.com/TD22057/insteon-mqtt/blob/master/insteon_mqtt/data/config-base.yaml) +[config-base.yaml](../insteon_mqtt/data/config-base.yaml) Settings in your user config file will replace those in the base config file pursuant to the following rules: diff --git a/docs/discovery.md b/docs/discovery.md index d8fb3a8a..8712269a 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -1,117 +1,86 @@ # MQTT Discovery Platform -HomeAssistant allows for InsteonMQTT to define entities using a discovery -protocol. This means, that for general installations, a user need only setup -InsteonMQTT following the -[Configuration Instructions](https://github.com/TD22057/insteon-mqtt/blob/master/docs/configuration.md) -and then follow the brief enabling instructions below to get Insteon working -in HomeAssistant. +Home Assistant's MQTT Integration supports device discovery, a +mechanism that allows services like Insteon-MQTT to *push* information +about the devices they support into Home Assistant. In many cases, a +user of Insteon-MQTT will only need to follow the basic [Configuration +Instructions](configuration.md), and then use the brief instructions +below to enable the discovery mechanism. + +With those configurations in place, all of the devices known to Insteon-MQTT +will automatically appear in Home Assistant, using a default configuration +for each device. If desired, each device's configuration can be customized, +either in Home Assistant or in the Insteon-MQTT configuration files. - [MQTT Discovery Platform](#mqtt-discovery-platform) - - [Enabling the Discovery Platform](#enabling-the-discovery-platform) + - [Enabling the discovery platform](#enabling-the-discovery-platform) + - [Insteon-MQTT 1.0.0 and later](#insteon-mqtt-100-and-later) + - [Insteon-MQTT 0.8.3 or earlier](#insteon-mqtt-083-or-earlier) - [Customization](#customization) - - [Altering entities in HomeAssistant](#altering-entities-in-homeassistant) - - [Disabling Entities in HomeAssistant](#disabling-entities-in-homeassistant) - - [Deleting Entities in HomeAssistant](#deleting-entities-in-homeassistant) - [Writing your own templates](#writing-your-own-templates) - [Default Device Templates](#default-device-templates) - [Using a Custom Device Template](#using-a-custom-device-template) - [Writing a `discovery_entities` Template](#writing-a-discovery_entities-template) - [JSON Dangers](#json-dangers) - [Passing Jinja Templates as Values](#passing-jinja-templates-as-values) - - [Example `discovery_entities` templates](#example-discovery_entities-templates) - - [The Special `device_info_template` Variable](#the-special-device_info_template-variable) - - [Sample Templates for Custom Discovery Classes](#sample-templates-for-custom-discovery-classes) - - [Single Button Remote](#single-button-remote) - - [Six Button Keypadlinc](#six-button-keypadlinc) - - [Setting Switches as Lights](#setting-switches-as-lights) + - [Example `discovery_entities`](#example-discovery_entities) -## Enabling the Discovery Platform +## Enabling the discovery platform + +### Insteon-MQTT 1.0.0 and later + +If your Insteon-MQTT configuration was built using version 1.0.0 (or +any later version), enabling the disovery platform requires a single +configuration setting in your `config.yaml` file: -Enabling the discovery platform for new installations is very easy. All you -need to do is set the following configuration setting: -to true: ```YAML mqtt: enable_discovery: true ``` -The base configuration file that ships inside InsteonMQTT contains the initial templates for all Insteon devices. - -> __If you installed InsteonMQTT starting with version 0.8.3 or earlier__, you will -need to read the [Migrating to Discovery](migrating.md) page for instructions on how to -incorporate the changes to the `config.yaml` file into your configuration. - -## Customization - -If the default entities defined by InsteonMQTT do not suit your needs, you may -be able to alter the entities within HomeAssistant. - -### Altering entities in HomeAssistant - -To do this, go to -`Configuration -> Integrations` find the MQTT integration and click on the -entities link. - -This page will contain a list of all of the defined entities. Find the one you -wish to alter and click on it. This settings page allows you to change the -name (only used in the UI) of the entity, the icon used in the UI, and the -entity ID that is used when referencing the entity in automations and in the -frontend. Under advanced settings, you can also change the area of the Device. -Click update to save your changes. +Enabling the platform, then restarting Insteon-MQTT, will result in +the __default__ device and entity templates (which are included in the +[config-base.yaml](../insteon_mqtt/data/config-base.yaml) file that +is part of the Insteon-MQTT installation) +being used to *push* those devices and entities to Home Assistant. -### Disabling Entities in HomeAssistant +### Insteon-MQTT 0.8.3 or earlier -It may also be the case that InsteonMQTT has defined a number of entities that -you do not need. For example, your keypadlinc may only have 6 buttons but 9 -are defined. +If your Insteon-MQTT configuration was built using any version before +1.0.0, you will need to read the [Migrating to +Discovery](migrating.md) page for instructions on how to incorporate +the necessary changes into your `config.yaml` file, and into your Home +Assistant configurations. -To remove the extra buttons, go to -`Configuration -> Integrations` find the MQTT integration and click on the -entities link. This page will contain a list of all of the defined entities. -Find the one you wish to disable and click on it. - -To remove the extra buttons, simply toggle tne `enable entity` setting. The -device will now no longer be listed in the UI, and will not show up in the -logbook or history. - -### Deleting Entities in HomeAssistant +## Customization -If you remove a device from your insteon network, or in some cases change how -it is defined, you will end up with old entities in HomeAssistant. To remove -an abandoned entity, make sure you remove it from the `devices` section of the -InsteonMQTT `yaml` config file. Then restart InsteonMQTT. You then need to -restart HomeAssistant. +If the default devices and entities created by Insteon-MQTT do not +suit your needs, there are two methods available for customization. -Then go to, `Configuration -> Integrations` find the MQTT integration and -click on the entities link. This page will contain a list of all of the -defined entities. Find the one you wish to delete and click on it. Abandoned -devices are easy to find because of the red icon on the right side. +One option is to edit the devices and entities in the Home Assistant +UI, and a guide for doing that is in the [Discovery Customization in +Home Assistant](discovery_customization_ui.md) page. -Inside, the settings page, click the Delete button. If the delete button is -disabled, then you have not 1) removed the device from your InsteonMQTT config, -2) restarted InsteonMQTT, OR 3) restared HomeAssistant. +The other option is to use 'overrides' in the Insteon-MQTT +configuration itself, so that the customized devices and entities are +sent to Home Assistant directly. A guide for using the 'overrides' +feature is in the [Discovery Customization in +Insteon-MQTT](discovery_customization_config.md) page. ## Writing your own templates -To understand how HomeAssistant discovery works, read more about the -[HomeAssistant Discovery Protocol](https://www.home-assistant.io/docs/mqtt/discovery/). - -Tweaking, editing, or adding to the default -configuration and can be done using [Jinja templates](https://github.com/TD22057/insteon-mqtt/blob/master/docs/Templating.md). - ### Default Device Templates -Discovery Device Templates are contained in your `yaml` config file. They are -defined using the `discovery_entities` key. By -default, a device will look to its corresponding subkey under the `mqtt` key. -So for example, a dimmer device will by default look to the `dimmer` subkey -under the `mqtt` key: +Discovery Device Templates are contained in your `yaml` config +file. They are defined using the `discovery_entities` key. By default, +a device will look to its corresponding subkey under the `mqtt` key. +So for example, a dimmer device will by default look to the `dimmer` +subkey under the `mqtt` key: ```YAML insteon: @@ -126,13 +95,17 @@ mqtt: # default ``` -If you review the contents of the base configuration file, under the `mqtt` section, you will see many examples of the `discovery_entities` setting. [config-base.yaml](https://github.com/TD22057/insteon-mqtt/blob/master/insteon_mqtt/data/config-base.yaml) +If you review the contents of the base configuration file, under the +`mqtt` section, you will see many examples of the `discovery_entities` +setting. [config-base.yaml](../insteon_mqtt/data/config-base.yaml) ### Using a Custom Device Template -Each device can also define a distinct template for its discovery entities. -This is done using [Device Specific Configuration](https://github.com/TD22057/insteon-mqtt/blob/master/docs/config_extra.md). -Specifically, using the `discovery_class` key. So you can do the following: +Each device can also define a distinct template for its discovery +entities. This is done using [Device Specific +Configuration](config_extra.md). Specifically, using the +`discovery_class` key. So you can do the following: + ```yaml insteon: device: @@ -147,343 +120,157 @@ mqtt: # default ``` -This class can be reused by any number of devices. Any device that uses the -entry `discovery_class: my_discovery_class` will look to this class. +This class can be reused by any number of devices. Any device that +uses the entry `discovery_class: my_discovery_class` will look to this +class. ### Writing a `discovery_entities` Template -The `discovery_entities` key should contain a list. Each list entry will -generate an entity in HomeAssistant. Some devices may only have one entity, -other devices may have multiple entities. - -Each entry in `discovery_entities` is an associative array with the __required__ -keys `component` and `config`. -- `component` - (String) One of the supported HomeAssistant MQTT components, -eg. `binary_sensor`, `light`, `switch` -- `config` - (jinja template) The template must produce a __json__ string that is -acceptable to HomeAssistant. The contents of what is required in this json -string are defined by the -[HomeAssistant Discovery Platform](https://www.home-assistant.io/docs/mqtt/discovery/). - -> The `config` json template __must include__ an entry for `unique_id` or -`uniq_id` containing a unique id for this entity. It is __strongly -recommended__ that you use the the device address as part of this unique id. -The recommended format is `{{address}}_suffix` where the suffix is something -that plainly describes the nature of this enity. Devices with only a single -entity do not need a suffix, but it is still good practice to use one. - -The `config` template has a number of variables available to it. For all -devices this includes at minimum the following, devices may also add -additional variables unique to these devices: - -- `name` = (str) device name in lower case +The `discovery_entities` key must contain an associative array. Each +array entry will generate an entity in Home Assistant. Some devices +may only have one entity, other devices may have multiple entities. + +Each entry in `discovery_entities` has a name; this name is used +__only__ inside the Insteon-MQTT configuration system, it is not +communicated to Home Assistant. + +Each entry in `discovery_entities` is an associative array with the +__required__ keys `component` and `config`. + +- `component` - (str) One of the supported Home Assistant MQTT components, +eg. `binary_sensor`, `light`, `switch` [See here for a full list](https://www.home-assistant.io/docs/mqtt/discovery/) + +- `config` - (associative array) The array is rendered into a __JSON__ +string that is acceptable to Home Assistant. The contents of what is +required in this JSON string are defined by the [Home Assistant +Discovery +Platform](https://www.home-assistant.io/docs/mqtt/discovery/). + + - Each entry in this array is processed as a Jinja2 template, which +means that variable substitution and template logic can be used. + +> The `config` array __must include__ an entry for `unique_id` +or `uniq_id` containing a unique id for this entity. It is __strongly +recommended__ that you use the the device address as part of this +unique id. The recommended format is `{{address}}_suffix` where the +suffix is something that plainly describes the nature of this entity. +Devices with only a single entity do not need a suffix, but it is +still good practice to use one. The unique id is used internally by +HomeAssistant and is not otherwise visible to the user. + +The `config` array has a number of variables available to it. For +all devices this includes at minimum the following, devices may also +add additional variables unique to these devices: + +- `name` = (str) device name in lower case. + +>This should not contain +the name of the device. HomeAssistant may produce an error if the +entity name starts with the device name, See: +[HomeAssistant Entity Naming Guidelines](https://developers.home-assistant.io/docs/core/entity/#entity-naming), +[Change in MQTT Naming](https://github.com/home-assistant/core/pull/95159), and +[Forum Discussion](https://community.home-assistant.io/t/psa-mqtt-name-changes-in-2023-8/598099). + - `address` = (str) hexadecimal address of device as a string + - `name_user_case` = (str) device name in the case entered by the user + - `engine` = (str) device engine version (e.g. i1, i2, i2cs). Will return `Unknown` if unknown. + - `model_number` = (str) device model number (e.g. 2476D). Will return `Unknown` if unknown. + - `model_description` = (str) description (e.g. SwitchLinc Dimmer) Will return `Unknown` if unknown. + - `firmware` = (int) device firmware version. Will return 0 by default + - `dev_cat` = (int) device category. Will return 0 by default + - `dev_cat_name` = (str) device category name Will return `Unknown` if unknown. + - `sub_cat` = (int) device sub-category. Will return 0 by default + - `modem_addr` = (str) hexadecimal address of modem as a string -- `device_info_template` = (jinja template) a template defined in -config.yaml. _See below_ -- `availability_topic` = The _availabiltiy_topic_ string as defied in the config.yaml file under the _mqtt_ key. + +- `device` = (str) a string which should bring in the standard device + information; the recommended value is + "{{device_info}}". Documentation of the `device_info` value can be + found [here](discovery_customization_config.md#defaults). + +- `availability_topic` = (str) the _availabiltiy_topic_ string as defined in + the config.yaml file under the _mqtt_ key. + - `<>` = (str) topic keys as defined in the config.yaml file under the _default class_ for this device are available as variables. -> The `<>` available are __always__ those listed under the default -class for this device. So for a `dimmer` the topics will be gathered from -the `mqtt->dimmer` key. Topics listed under a user defined `discovery_class` -will be ignored. +> The `<>` available are __always__ those listed under the +default class for this device. So for a `dimmer` the topics will be +gathered from the `mqtt->dimmer` key. Topics listed under a +user-defined `discovery_class` will be ignored. -Additional variables may be offered by specific devices classes. Those -variables are defined in the `config-example.yaml` file under the relevant -`mqtt` device keys. +Additional variables may be offered by specific device classes. +Those variables are defined in the [config-base.yaml](../insteon_mqtt/data/config-base.yaml) +file under the relevant `mqtt` device keys. #### JSON Dangers -> The `config` json template __must generate valid json__. This is a good json -[validator](https://jsonformatter.curiousconcept.com/). +> The entries in the `config` array __must generate valid JSON__. This is a +good JSON [validator](https://jsonformatter.curiousconcept.com/). __Notable Gotchas__ -1. __Newline Characters__ - JSON strings cannot contain raw newline characters, -they can however be represented by `\n`. Keep in mind that the config template -is first injested from yaml. You can read about -[how yaml handles whitespace](https://yaml-multiline.info/). Second, the -config template is rendered through Jinja. You can read about -[how jinja handles whitespace](https://tedboy.github.io/jinja2/templ6.html). -2. __Trailing Commas__ - JSON cannot include trailing commas. The last item -in a list or the last key:value pair in an object __cannot__ be followed by a -comma. -3. __Single Quotes__ - JSON requires doubles quotes, you __cannot__ use single -quotes to define a string. You can escape double quotes with `\"` +1. __Newline Characters__ - JSON strings cannot contain raw newline +characters, they can however be represented by `\n`. Keep in mind that +the config template is first ingested from YAML. You can read about +[how yaml handles whitespace](https://yaml-multiline.info/). Second, +the config array entries are rendered through Jinja2. You can read +about [how jinja handles +whitespace](https://tedboy.github.io/jinja2/templ6.html). + +1. __Single Quotes__ - JSON requires doubles quotes, you __cannot__ +use single quotes to define a string. You can escape double quotes +with `\"` #### Passing Jinja Templates as Values -HomeAssistant uses jinja templates as well, and in a number of cases entities -have configuration settings that contain a template. If you attempt to enter a -template as a value, it will be rendered by InsteonMQTT, which in this case -would likely result with an empty value. +Home Assistant uses Jinja2 templates as well, and in a number of cases +entities have configuration settings that contain a template. If you +attempt to enter a template as a value, it will be rendered by +Insteon-MQTT, which in this case would likely result in an empty +value. -To pass an unrendered template on to HomeAssistant __you must escape the -template__. The template can be escaped using the `{% raw %} {{escaped_stuff}} {% endraw %}` -format. For example: +To pass an unrendered template on to Home Assistant __you must escape +the template__. The template can be escaped using the `{% raw %} +{{escaped_stuff}} {% endraw %}` format. For example: ```yaml mqtt: climate: discovery_entities: - - component: "climate" - config: |- - { + sensor: + component: "climate" + config: .... # other settings - "temp_lo_stat_tpl": "{% raw %}{{value_json.temp_f}}{% endraw %}", - } + temp_lo_stat_tpl: "{% raw %}{{value_json.temp_f}}{% endraw %}" ``` -#### Example `discovery_entities` templates +#### Example `discovery_entities` ```yaml mqtt: - # Other keys ommitted + # Other keys omitted dimmer: # Other keys omitted discovery_entities: - - component: 'light' - config: |- - { - "uniq_id": "{{address}}_light", - "name": "{{name_user_case}}", - "cmd_t": "{{level_topic}}", - "stat_t": "{{state_topic}}", - "brightness": true, - "schema": "json", - "device": {{device_info_template}} - } -``` - -### The Special `device_info_template` Variable -Inside HomeAssistant each entity config can contain a description about the -device that the entity is contained in. This is mostly a cosmetic feature -that provides some level of topology to HomeAssistant and can allow you to -see all of the entities on a single device. - -This device description configuration is likely going to use an identical -template from one device to the next. To make things easier, the subkey -`device_info_template` can be defined under the `mqtt` key. The contents -of this key should be a template that when rendered produces the device_info -relevant to the majority of your devices. This template can then be inserted -into any of the `discovery_entities` by using the `device_info_template` -variable. - -You can view the default template in [config-base.yaml](https://github.com/TD22057/insteon-mqtt/blob/master/insteon_mqtt/data/config-base.yaml) - -For example, the following a complex template that produces a nice device -info: -```YAML -mqtt: - # Other keys omitted - device_info_template: |- - { - "ids": "{{address}}", - "mf": "Insteon", - "mdl": "{%- if model_number != 'Unknown' -%} - {{model_number}} - {{model_description}} - {%- elif dev_cat_name != 'Unknown' -%} - {{dev_cat_name}} - 0x{{'%0x' % sub_cat|int }} - {%- elif dev_cat == 0 and sub_cat == 0 -%} - No Info - {%- else -%} - 0x{{'%0x' % dev_cat|int }} - 0x{{'%0x' % sub_cat|int }} - {%- endif -%}", - "sw": "0x{{'%0x' % firmware|int }} - {{engine}}", - "name": "{{name_user_case}}", - "via_device": "{{modem_addr}}" - } -``` - -This when used in a `discovery_entities` template described above will render -as: - -```JSON -{ - "uniq_id": "4f.23.38_light", - "name": "my dimmer", - "cmd_t": "insteon/4f.23.38/level", - "stat_t": "insteon/4f.23.38/state", - "brightness": true, - "schema": "json", - "device": { - "ids": "4f.23.38", - "mf": "Insteon", - "mdl": "2477D - SwitchLinc Dimmer (Dual-Band)", - "sw": "0x45 - i2cs", - "name": "my dimmer", - "via_device": "41.ee.e6" - } -} -``` - -## Sample Templates for Custom Discovery Classes - -### Single Button Remote - -The default remote configuration exposes entities for all eight -buttons. However, if you have a single button remote, you likely -only want to see an entity for that single button. The following -sample configuration settings will enable that: - -```yaml -insteon: - device: - mini_remote1:: - - dd.ee.ff: my_remote - discovery_class: remote_1 # < note no dash at start of line - -mqtt: - remote_1: # < Note the class name - discovery_entities: - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_btn", - "name": "{{name_user_case}} btn", - "stat_t": "{{state_topic_1}}", - "device": {{device_info_template}} - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_battery", - "name": "{{name_user_case}} battery", - "stat_t": "{{low_battery_topic}}", - "device_class": "battery", - "device": {{device_info_template}} - } - - component: 'sensor' - config: |- - { - "uniq_id": "{{address}}_heartbeat", - "name": "{{name_user_case}} heartbeat", - "stat_t": "{{heartbeat_topic}}", - "device_class": "timestamp", - "device": {{device_info_template}} - } + light: + component: 'light' + config: + uniq_id: "{{address}}_light" + name: "{{name_user_case}}" + cmd_t: "{{level_topic}}" + stat_t: "{{state_topic}}" + brightness: true + schema: "json" + device: "{{device_info}}" ``` - -### Six Button Keypadlinc - -The default Keypad_linc configuration exposes entities for all eight -buttons. However, if you have a six button keypad_linc, you likely -only want to see entities for those six buttons. The following -sample configuration settings will enable that: - -```yaml -insteon: - device: - keypad_linc:: - - 11.22.33: my_6_button_kpl - discovery_class: kpl_6 # < note no dash at start of line - -mqtt: - kpl_6: # < Note the class name - discovery_entities: - - component: 'light' - config: |- - { - "uniq_id": "{{address}}_1", - "name": "{{name_user_case}} btn 1", - "device": {{device_info_template}}, - "brightness": {{is_dimmable|lower()}}, - "cmd_t": "{%- if is_dimmable -%} - {{dimmer_level_topic}} - {%- else -%} - {{btn_on_off_topic_1}} - {%- endif -%}", - "schema": "json", - "stat_t": "{%- if is_dimmable -%} - {{dimmer_state_topic}} - {%- else -%} - {{btn_state_topic_1}} - {%- endif -%}" - } - - component: 'switch' # No button 2 on 6 button devices - config: |- - { - "uniq_id": "{{address}}_3", - "name": "{{name_user_case}} btn 3", - "device": {{device_info_template}}, - "cmd_t": "{{btn_on_off_topic_3}}", - "stat_t": "{{btn_on_off_topic_3}}", - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_4", - "name": "{{name_user_case}} btn 4", - "device": {{device_info_template}}, - "cmd_t": "{{btn_on_off_topic_4}}", - "stat_t": "{{btn_on_off_topic_4}}", - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_5", - "name": "{{name_user_case}} btn 5", - "device": {{device_info_template}}, - "cmd_t": "{{btn_on_off_topic_5}}", - "stat_t": "{{btn_on_off_topic_5}}", - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_6", - "name": "{{name_user_case}} btn 6", - "device": {{device_info_template}}, - "cmd_t": "{{btn_on_off_topic_6}}", - "stat_t": "{{btn_on_off_topic_6}}", - } - # No buttons 7-9 on 6 button devices -``` - -### Setting Switches as Lights - -Switchlincs are by default defined as `switch` components in HomeAssistant. -However, you may prefer to define them as `light` components without the -dimming feature. This has a few benefits, 1) the component classification may -better match the actual use, 2) you get the nice lightbulb icon automatically, -3) when the entities are linked to devices such as Google Home or Amazon Alexa -HomeAssistant, they will appear within these platforms as lights. - -To do this, define a new custom `discovery_class` as follows: - -```yaml -mqtt: - switch_as_light: - # Maps a switch to a light, which is nicer in HA for actual lights - discovery_entities: - - component: "light" - config: >- - { - "uniq_id": "{{address}}_light", - "name": "{{name_user_case|title}}", - "cmd_t": "{{on_off_topic}}", - "stat_t": "{{state_topic}}", - "brightness": false, - "schema": "json", - "device": {{device_info_template}} - } -``` - -Then for each device just add the discovery class: - -```yaml -devices: - switch: - - aa.bb.cc: My Light - discovery_class: switch_as_light diff --git a/docs/discovery_customization_config.md b/docs/discovery_customization_config.md new file mode 100644 index 00000000..8bc0cea1 --- /dev/null +++ b/docs/discovery_customization_config.md @@ -0,0 +1,709 @@ +# Discovery Customization in Insteon-MQTT + + + +- [Discovery Customization in Insteon-MQTT](#discovery-customization-in-insteon-mqtt) + - [Background](#background) + - [Defaults](#defaults) + - [Overrides](#overrides) + - [Device-level overrides](#device-level-overrides) + - [Modifying](#modifying) + - [Hiding](#hiding) + - [Class-level overrides](#class-level-overrides) + - [Class-level override example: SwitchLinc Relay as Light](#class-level-override-example-switchlinc-relay-as-light) + - [Class-level override example: KeypadLinc](#class-level-override-example-keypadlinc) + - [Class-level override example: Mini Remote Switch](#class-level-override-example-mini-remote-switch) + - [Class-level override example: multiple classes](#class-level-override-example-multiple-classes) + - [Entity names for overrides](#entity-names-for-overrides) + - [Device type 'battery_sensor' entities](#device-type-battery_sensor-entities) + - [Device type 'dimmer' entities](#device-type-dimmer-entities) + - [Device type 'ezio4o' entities](#device-type-ezio4o-entities) + - [Device type 'fan_linc' entities](#device-type-fan_linc-entities) + - [Device type 'hidden_door' entities](#device-type-hidden_door-entities) + - [Device type 'io_linc' entities](#device-type-io_linc-entities) + - [Device type 'keypad_linc' entities](#device-type-keypad_linc-entities) + - [Device type 'keypad_linc_sw' entities](#device-type-keypad_linc_sw-entities) + - [Device type 'leak' entities](#device-type-leak-entities) + - [Device type 'mini_remote1' entities](#device-type-mini_remote1-entities) + - [Device type 'mini_remote4' entities](#device-type-mini_remote4-entities) + - [Device type 'mini_remote8' entities](#device-type-mini_remote8-entities) + - [Device type 'motion' entities](#device-type-motion-entities) + - [Device type 'smoke_bridge' entities](#device-type-smoke_bridge-entities) + - [Device type 'switch' entities](#device-type-switch-entities) + - [Device type 'outlet' entities](#device-type-outlet-entities) + + + +## Background + +To understand how Home Assistant's MQTT integration discovery works, +read more about the [Home Assistant Discovery +Protocol](https://www.home-assistant.io/docs/mqtt/discovery/). + +Insteon-MQTT's implementation of this protocol relies heavily on +[Jinja2 +templates](https://github.com/TD22057/insteon-mqtt/blob/master/docs/Templating.md). + +## Defaults + +The default entities produced by Insteon-MQTT discovery are built from +two templates: one for the device, and one for *each* entity supported +by the device. The templates are included in the +[`config-base.yaml`](../insteon_mqtt/data/config-base.yaml) file that +is part of the Insteon-MQTT installation. + +The template which produces the device information, which is common to all +devices supported by Insteon-MQTT, looks like this: + +```yaml +mqtt: + device_info_template: + ids: "{{address}}" + mf: "Insteon" + mdl: "{%- if model_number != 'Unknown' -%} + {{model_number}} - {{model_description}} + {%- elif dev_cat_name != 'Unknown' -%} + {{dev_cat_name}} - 0x{{'%0x' % sub_cat|int }} + {%- elif dev_cat == 0 and sub_cat == 0 -%} + No Info + {%- else -%} + 0x{{'%0x' % dev_cat|int }} - 0x{{'%0x' % sub_cat|int }} + {%- endif -%}" + sw: "0x{{'%0x' % firmware|int }} - {{engine}}" + name: "{{name_user_case}}" + via_device: "{{modem_addr}}" + +``` + +This template uses the shortened attribute names listed in the MQTT +Discovery documentation: `ids` instead of `identifiers`, `mf` instead +of `manufacturer`, etc. + +The templates which produce the entities for a specific device type +look like this (for a FanLinc): + +```yaml +mqtt: + fan_linc: + discovery_entities: + fan: + component: 'fan' + config: + uniq_id: "{{address}}_fan" + name: "{{name_user_case}} fan" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{fan_on_off_topic}}" + stat_t: "{{fan_state_topic}}" + stat_val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + pct_cmd_t: "{{fan_speed_set_topic}}" + pct_cmd_tpl: "{% raw %}{% if value < 10 %}off{% elif value < 40 %}low{% elif value < 75 %}medium{% else %}high{% endif %}{% endraw %}" + pct_stat_t: "{{fan_speed_topic}}" + pct_val_tpl: "{% raw %}{% if value == 'low' %}33{% elif value == 'medium' %}67{% elif value == 'high' %}100{% else %}0{% endif %}{% endraw %}" + pr_mode_stat_t: "{{fan_speed_topic}}" + pr_mode_cmd_t: "{{fan_speed_set_topic}}" + pr_modes: ["off", "low", "medium", "high"] + json_attr_t: "{{fan_state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + light: + component: 'light' + config: + uniq_id: "{{address}}_light" + name: "{{name_user_case}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{level_topic}}" + stat_t: "{{state_topic}}" + brightness: true + schema: "json" + device: "{{device_info}}" + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" +``` + +This set of templates produces two entities for each FanLinc device, +one named `fan` and one named `light`. Note that these names are +internal to the Insteon-MQTT configuration only, they are not exposed +to Home Assistant (Home Assistant uses the name from the `name` +attribute in the `config` block). + +Each entity provides a `component` attribute which tells Home +Assistant which type of component this entity should be mapped to, and +a `config` block which provides the details of the entity (in the MQTT +Discovery protocol, this `config` block become the payload of the +discovery message sent over MQTT). + +As with the `device_info_template`, these `config` blocks use the +shortened attribute names when available: `cmd_t` instead of +`command_topic`, `json_attr_t` instead of `json_attributes_topic`, +etc. + +A special attribute in the `config` block is the `device` attribute: +as shown above, its value is "{{device_info}}", which means it will +contain the result of rendering the `device_info_template` for the +device which contains this entity. All of the entities contained by +that device will have the same content in their `device` attributes, +and Home Assistant will use that information to link them with the +device. + +## Overrides + +There is an override mechanism available, which allows the +configuration in `config.yaml` to add, replace, or remove attributes +at both the device and entity levels; it also permits 'hiding' of +entities and entire devices so that they will not appear in Home +Assistant. + +Overrides are specified using a `discovery_overrides` block at the +appropriate level; those blocks look like this: + +```yaml +discovery_overrides: + discoverable: true/false + device: + # add an attribute named 'sa' + sa: "Office" + # replace the attribute named 'mf' + mf: "Custom" + # remove the attribute named 'sw' + sw: "" + : + discoverable: true/false + component: + config: + # add an attribute named 'icon' + icon: "mdi:ceiling-fan" + # replace the attribute named 'name' + name: "Office Fan" + # remove the attribute named 'cmd_t' + cmd_t: "" +``` + +When an overrides block is processed, each instruction in it is +applied to the attributes provided by the template used for the +device, or to the attributes left after a previous overrides block was +processed (in cases where multiple overrides blocks are used for the +same device). + +The available 'entity names' for each type of device are documented +at the bottom of this page. + +### Device-level overrides + +#### Modifying + +Specifying overrides at the device level looks like this: + +```yaml +insteon: + devices: + fan_linc: + - aa.bb.cc: "Office Fan" + discovery_overrides: + device: + sa: "Office" + fan: + config: + name: "Office Fan" + icon: "mdi:ceiling-fan" + light: + config: + name: "Office Fan Light" + icon: "mdi:ceiling-fan-light" +``` + +There are five overrides in this block: + +* The `sa` (`suggested_area`) of the entire device is set to + "Office". If the Home Assistant configuration includes an Area named + "Office", then this device and its entities will appear in the + dashboard section for that area. + +* The name of the `fan` entity of the device is set to "Office Fan", + which will be used instead of the default "Office Fan fan". + +* The icon for the `fan` entity of the device is set to + "mdi:ceiling-fan", which will be used instead of the default + "mdi:fan" icon in the Home Assistant dashboard. + +* The name of the `fan` entity of the device is set to "Office Fan Light", + which will be used instead of the default "Office Fan". + +* The icon for the `light` entity of the device is set to + "mdi:ceiling-fan-light", which will be used instead of the default + "mdi:lightbulb" icon in the Home Assistant dashboard. + +#### Hiding + +It is also possible to hide devices or entities from the discovery +system; if this FanLinc should not appear in Home Asssistant at all, +then: + +```yaml +insteon: + devices: + fan_linc: + - aa.bb.cc: "Office FanLinc" + discovery_overrides: + discoverable: false +``` + +Note that this only hides the device from Home Assistant; it is still +fully operational in Insteon-MQTT, so it can be controlled and queried +using MQTT messages, and it can participate in scenes defined in +`scenes.yaml`. + +If, on the other hand, the device should appear in Home Assistant, but +this particular fan does not have a light, then: + +```yaml +insteon: + devices: + fan_linc: + - aa.bb.cc: "Office FanLinc" + discovery_overrides: + light: + discoverable: false +``` + +This will hide *just* the light from Home Assistant, but not the fan. + +### Class-level overrides + +When the Insteon-MQTT configuration includes multiple devices which +require the same overrides, or even just a common subset of overrides, +it can be more efficient (and easier to maintain) to create a 'class' +which contains those overrides and gives them a useful name. + +This is done by adding a block to the `mqtt` section of `config.yaml`, +and including the necessary `discovery_overrides` content in that +block. + +```yaml +mqtt: + switch_as_light: + discovery_overrides: + switch: + component: "light" + config: + brightness: false + val_tpl: "" +``` + +This block provides three overrides for an entity named `switch` in +any device that it is applied to: + +* The `component` mapping is set to "light", replacing the default of + "switch". + +* The `brightness` attribute is set to "false", so that Home Assistant + will know that this 'light' can only be set to 'on' or 'off', not to + a brightness level. + +* The `val_tpl` attribute is set to an empty string, so that it will + be removed from the configuration data sent to Home Assistant + ('light' entities in Home Assistant do not support the `val_tpl` + attribute). + +Applying this new 'discovery override class' to multiple Insteon +SwitchLinc Relay devices is done this way: + +```yaml +insteon: + devices: + switch: + - dd.ee.01: "Office Closet" + discovery_override_class: "switch_as_light" + - dd.ee.02: "Attic" + discovery_override_class: "switch_as_light" + - dd.ee.03: "Basement" + discovery_override_class: "switch_as_light" +``` + +The `discovery_override_class` attribute of the device is used to +indicate the name(s) of classes of discovery override data which +should be applied to this device during MQTT Discovery. The classes +are applied in the order they are specified. Finally, any +`discovery_overrides` specified at the device level are applied. + +The result is that all three of these devices are reported to Home +Assistant as 'on-off lights', rather than generic switches. + +#### Class-level override example: SwitchLinc Relay as Light + +Repeating (and condensing) the example from the last section: + +The desired result is for a series of SwitchLinc Relay devices to +appear in Home Assistant as lights, instead of generic switches. + +```yaml +insteon: + devices: + switch: + - dd.ee.01: "Office Closet" + discovery_override_class: "switch_as_light" + - dd.ee.02: "Attic" + discovery_override_class: "switch_as_light" + - dd.ee.03: "Basement" + discovery_override_class: "switch_as_light" + +mqtt: + switch_as_light: + discovery_overrides: + switch: + component: "light" + config: + brightness: false + val_tpl: "" +``` + +#### Class-level override example: KeypadLinc + +A common situation is that Insteon-MQTT is configured to manage a +KeypadLinc that has a 6-button faceplate, not an 8-button +faceplace. Since the default `discovery_entities` for a KeypadLinc +assume there are 8 buttons (plus a virtual 9th button to be used if +the KeypadLinc has been configured with a 'detached load'), Home +Assistant will show switches for the device that do not actually +exist. + +An example `discovery_override_class` overrides block to hide the +unneeded entities could look like this: + +```yaml +insteon: + devices: + keypad_linc: + - dd.ee.01: "Office" + discovery_override_class: "kpl_6_buttons" + keypad_linc_sw: + - dd.ee.02: "Garage" + discovery_override_class: "kpl_6_buttons" + +mqtt: + kpl_6_buttons: + discovery_overrides: + button2: + discoverable: false + button7: + discoverable: false + button8: + discoverable: false + button9: + discoverable: false +``` + +This overrides block contains four overrides, each of which 'hide' a +specific entity from the default list of entities for a +KeypadLinc. The example shows this being applied to both a +dimmer-style KeypadLinc and a relay-style (on/off) KeypadLinc. + +#### Class-level override example: Mini Remote Switch + +Insteon produces three types of battery-powered wireless remote +controllers: the Mini Remote Switch, the Mini Remote (4 scene), and +the Mini Remote. They all operate identically and Insteon-MQTT +supports them all equally, but they have different numbers of buttons +(1, 4, and 8, respectively). + +When a Mini Remote Switch is configured in `config.yaml`, the result +will be eight 'binary_sensor' entities appearing in Home Assistant +(along with a 'battery' entity used to monitor the battery power +level), but seven of them are not useful. + +An example `discovery_override_class` overrides block to hide the +unneeded entities could look like this: + +```yaml +insteon: + devices: + mini_remote1: + - dd.ee.01: "Stairs" + discovery_override_class: "remote_1_button" + +mqtt: + remote_1_button: + discovery_overrides: + button2: + discoverable: false + button3: + discoverable: false + button4: + discoverable: false + button5: + discoverable: false + button6: + discoverable: false + button7: + discoverable: false + button8: + discoverable: false +``` + +This overrides block contains seven overrides, each of which hides the +corresponding button so that it will not appear in Home Assistant. The +'battery' entity for the device is not affected by these overrides. + +#### Class-level override example: multiple classes + +Combining some of the examples above, the Insteon network might +include a number of dimmers, FanLincs, KeypadLincs, and Mini Remotes +in a large 'game room'. Some of those devices will require overrides +to change entity names or hide entities that are not usable; all of +them require an override to set the 'suggested area'. + +```yaml +insteon: + devices: + dimmer: + - bb.bb.01: "North End Lights" + discovery_override_class: "game_room" + - bb.bb.02: "South End Lights" + discovery_override_class: "game_room" + fan_linc: + - cc.cc.01: "North Fan" + discovery_override_class: + - "fan_name_icons" + - "game_room" + - cc.cc.02: "South Fan" + discovery_override_class: + - "fan_name_icons" + - "game_room" + keypad_linc: + - dd.dd.01: "North Fan" + discovery_override_class: + - "kpl_6_buttons" + - "game_room" + - dd.dd.02: "South Fan" + discovery_override_class: + - "kpl_6_buttons" + - "game_room" + mini_remote1: + - ee.ee.01: "North End Drapes" + discovery_override_class: + - "remote_1_button" + - "game_room" + - ee.ee.02: "South End Drapes" + discovery_override_class: + - "remote_1_button" + - "game_room" + +mqtt: + game_room: + discovery_overrides: + device: + sa: "Game Room" + fan_name_icons: + discovery_overrides: + fan: + config: + name: "Office Fan" + icon: "mdi:ceiling-fan" + light: + config: + name: "Office Fan Light" + icon: "mdi:ceiling-fan-light" + kpl_6_buttons: + discovery_overrides: + button2: + discoverable: false + button7: + discoverable: false + button8: + discoverable: false + button9: + discoverable: false + remote_1_button: + discovery_overrides: + button2: + discoverable: false + button3: + discoverable: false + button4: + discoverable: false + button5: + discoverable: false + button6: + discoverable: false + button7: + discoverable: false + button8: + discoverable: false +``` + +With all of these overrides in place, this group of eight devices will +appear in the "Game Room" Area in Home Assistant; only the necessary +(and useful) entities will appear, and the FanLinc entities will have +custom names and icons. + +## Entity names for overrides + +Each section below documents the default entities produced during +MQTT Discovery for each type of device (of group of types) supported +by Insteon-MQTT. + +### Device type 'battery_sensor' entities + +|Name|Component Type|Purpose| +|---|---|---| +|door|binary_sensor|open/closed sensor| +|battery|binary_sensor|battery good/low| +|heartbeat|sensor|regular update from device to confirm communication| + +### Device type 'dimmer' entities + +|Name|Component Type|Purpose| +|---|---|---| +|dimmer|light|lever/paddle dimmer control| + +### Device type 'ezio4o' entities + +|Name|Component Type|Purpose| +|---|---|---| +|relay1|switch|low-voltage relay 1| +|relay2|switch|low-voltage relay 2| +|relay3|switch|low-voltage relay 3| +|relay4|switch|low-voltage relay 4| + +### Device type 'fan_linc' entities + +|Name|Component Type|Purpose| +|---|---|---| +|fan|fan|multi-speed fan controller| +|light|light|dimmable light controller| + +### Device type 'hidden_door' entities + +|Name|Component Type|Purpose| +|---|---|---| +|door|binary_sensor|open/closed sensor| +|battery|binary_sensor|battery good/low| +|heartbeat|sensor|regular update from device to confirm communication| +|voltage|sensor|battery voltage| + +### Device type 'io_linc' entities + +|Name|Component Type|Purpose| +|---|---|---| +|relay|switch|low-voltage relay| +|sensor|binary_sensor|contact closure input| + +### Device type 'keypad_linc' entities + +Note that Home Assistant control of buttons 2-8 will +only turn the button LEDs on and off; it will not +trigger the responders that have been linked (in a scene) +to the buttons. + +|Name|Component Type|Purpose| +|---|---|---| +|button1|light|'On'on 6-button, 'A' on 8-button| +|button2|switch|not usable on 6-button, 'B' on 8-button| +|button3|switch|'A' on 6-button, 'C' on 8-button| +|button4|switch|'B' on 6-button, 'D' on 8-button| +|button5|switch|'C' on 6-button, 'E' on 8-button| +|button6|switch|'D' on 6-button, 'F' on 8-button| +|button7|switch|not usable on 6-button, 'G' on 8-button| +|button8|switch|not usable on 6-button, 'H' on 8-button| +|button9|switch|not currently usable in Insteon-MQTT| + +### Device type 'keypad_linc_sw' entities + +Note that Home Assistant control of buttons 2-8 will +only turn the button LEDs on and off; it will not +trigger the responders that have been linked (in a scene) +to the buttons. + +|Name|Component Type|Purpose| +|---|---|---| +|button1|light (with `brightness` set to "false")|'On'on 6-button, 'A' on 8-button| +|button2|switch|not usable on 6-button, 'B' on 8-button| +|button3|switch|'A' on 6-button, 'C' on 8-button| +|button4|switch|'B' on 6-button, 'D' on 8-button| +|button5|switch|'C' on 6-button, 'E' on 8-button| +|button6|switch|'D' on 6-button, 'F' on 8-button| +|button7|switch|not usable on 6-button, 'G' on 8-button| +|button8|switch|not usable on 6-button, 'H' on 8-button| +|button9|switch|not currently usable in Insteon-MQTT| + +### Device type 'leak' entities + +|Name|Component Type|Purpose| +|---|---|---| +|wet|binary_sensor|wet/dry sensor| +|heartbeat|sensor|regular update from device to confirm communication| + +### Device type 'mini_remote1' entities + +|Name|Component Type|Purpose| +|---|---|---| +|button1|binary_sensor|paddle switch| +|button2|binary_sensor|not usable| +|button3|binary_sensor|not usable| +|button4|binary_sensor|not usable| +|button5|binary_sensor|not usable| +|button6|binary_sensor|not usable| +|button7|binary_sensor|not usable| +|button8|binary_sensor|not usable| +|battery|binary_sensor|battery good/low| + +### Device type 'mini_remote4' entities + +|Name|Component Type|Purpose| +|---|---|---| +|button1|binary_sensor|'a' switch| +|button2|binary_sensor|'b' switch| +|button3|binary_sensor|'c' switch| +|button4|binary_sensor|'d' switch| +|button5|binary_sensor|not usable| +|button6|binary_sensor|not usable| +|button7|binary_sensor|not usable| +|button8|binary_sensor|not usable| +|battery|binary_sensor|battery good/low| + +### Device type 'mini_remote8' entities + +|Name|Component Type|Purpose| +|---|---|---| +|button1|binary_sensor|'a' switch| +|button2|binary_sensor|'b' switch| +|button3|binary_sensor|'c' switch| +|button4|binary_sensor|'d' switch| +|button5|binary_sensor|'e' switch| +|button6|binary_sensor|'f' switch| +|button7|binary_sensor|'g' switch| +|button8|binary_sensor|'h' switch| +|battery|binary_sensor|battery good/low| + +### Device type 'motion' entities + +|Name|Component Type|Purpose| +|---|---|---| +|motion|binary_sensor|motion sensor| +|battery|binary_sensor|battery good/low| +|dusk|binary_sensor|dawn/dusk (light level) sensor| + +### Device type 'smoke_bridge' entities + +|Name|Component Type|Purpose| +|---|---|---| +|smoke|binary_sensor|smoke sensor| +|battery|binary_sensor|battery good/low| +|co|binary_sensor|carbon monoxide sensor| +|error|binary_sensor|operational error| + +### Device type 'switch' entities + +|Name|Component Type|Purpose| +|---|---|---| +|switch|switch|toggle/paddle switch| + +### Device type 'outlet' entities + +|Name|Component Type|Purpose| +|---|---|---| +|top|switch|upper receptacle| +|bottom|switch|lower receptacle| diff --git a/docs/discovery_customization_ui.md b/docs/discovery_customization_ui.md new file mode 100644 index 00000000..5e24dc65 --- /dev/null +++ b/docs/discovery_customization_ui.md @@ -0,0 +1,290 @@ +# Discovery Customization in Home Assistant + + + +- [Discovery Customization in Home Assistant](#discovery-customization-in-home-assistant) + - [Access to configuration](#access-to-configuration) + - [Altering devices](#altering-devices) + - [Altering entities](#altering-entities) + - [Disabling entities](#disabling-entities) + - [Deleting devices](#deleting-devices) + - [Entity names](#entity-names) + - [Device type 'battery_sensor' entities](#device-type-battery_sensor-entities) + - [Device type 'dimmer' entities](#device-type-dimmer-entities) + - [Device type 'ezio4o' entities](#device-type-ezio4o-entities) + - [Device type 'fan_linc' entities](#device-type-fan_linc-entities) + - [Device type 'hidden_door' entities](#device-type-hidden_door-entities) + - [Device type 'io_linc' entities](#device-type-io_linc-entities) + - [Device type 'keypad_linc' entities](#device-type-keypad_linc-entities) + - [Device type 'keypad_linc_sw' entities](#device-type-keypad_linc_sw-entities) + - [Device type 'leak' entities](#device-type-leak-entities) + - [Device type 'mini_remote1' entities](#device-type-mini_remote1-entities) + - [Device type 'mini_remote4' entities](#device-type-mini_remote4-entities) + - [Device type 'mini_remote8' entities](#device-type-mini_remote8-entities) + - [Device type 'motion' entities](#device-type-motion-entities) + - [Device type 'smoke_bridge' entities](#device-type-smoke_bridge-entities) + - [Device type 'switch' entities](#device-type-switch-entities) + - [Device type 'outlet' entities](#device-type-outlet-entities) + + + +## Access to configuration + +All configuration of the devices and entities discovered from +Insteon-MQTT is done in the MQTT integration. To access the +integration in the Home Assistant UI: + +1. Click the 'Configuration' menu item in the sidebar. + +1. Click 'Devices and Services' in the Configuration pane. + +1. If the 'Integrations' item in the top menu bar is not highlighted, +click it to access the installed integrations. + +1. Locate the MQTT integration (which may require scrolling if there +are a large number of integrations installed). + +1. In the MQTT integration panel there will be two links: one for +devices, and one for entities. Each of these links will lead to the +respective section of the Configuration pane, with the list filtered +to show only the items provided by the MQTT integration. + +### Altering devices + +In the device list, find the one you wish to alter and click on it. A +new pane will appear with the details of the device. + +Click the pencil icon next to the device name; in the dialog that +appears, the name of the device and its Area can be set or +modified. Make any changes necessary, then click the 'Update' link in +the bottom-right corner of the dialog. + +To return to the device list, click the left arrow in the top menu bar. + +### Altering entities + +In the entity list, find the one you wish to alter and click on it. In +the dialog that appears, the name of the entity, the icon displayed +for the entity, and the 'Entity ID' can be set or modified. If the +entity should be listed in a different Area from its containing +device, click the 'Advanced settings' section to open it, and set the +desired Area in the 'Set entity area only' field. + +Make any changes necessary, then click the 'Update' link in the +bottom-right corner of the dialog. + +To return to the entity list, click the left arrow in the top menu bar. + +### Disabling entities + +In some cases the entity list for a device discovered from +Insteon-MQTT may contain entities which are not applicable to your +situation. For example the defeult entity list for a KeypadLinc +includes 9 buttons, but if the installed KeypadLinc has a 6-button +faceplate, there will be three extra entities listed for it. + +A similar situation may occur for a Mini Remote Switch or a Mini +Remote (4 Scene), the entity list will contain 8 buttons by default. + +In the entity list, find the one you wish to disable and click on +it. In the dialog that appears, click the 'Enable entity' slider to +turn it off. Click the 'Update' link in the bottom-right corner of the +dialog. + +This process can be reversed to re-enable a disabled entity, although +Home Assistant will need to be restarted to complete the process (and +the MQTT integration will display a reminder). + +To return to the entity list, click the left arrow in the top menu bar. + +### Deleting devices + +If you remove a device from your insteon network, or in some cases +change how it is defined, you will end up with a 'stale' device in +Home Assistant. To remove an abandoned device, make sure you remove +it from the `devices` section of the Insteon-MQTT `config.yaml` file, +then restart Insteon-MQTT. After it has restarted, restart Home +Assistant. + +In the device list, find the one you wish to delete and click on it. A +new pane will appear with the details of the device. + +In the device settings pane, click the Delete button. A dialog will +appear to confirm the deletion request; once the device has been +deleted, Home Assistant will display 'Device / service not found.' +with a 'GO BACK' link. Click that link to return to the device list. + +## Entity names + +Each section below documents the entities produced during MQTT +Discovery for each type of device (of group of types) supported by +Insteon-MQTT. In the tables below, 'NAME' is substituted with the +name specified in `config.yaml` for the device, after conversion +to Home Assistant's internal name format (all lowercase, spaces +replaced by underscores, etc). + +As an example, a FanLinc device named "Game Room" in `config.yaml` +will produce two entities: 'fan.game_room_fan' and 'light.game_room'. + +### Device type 'battery_sensor' entities + +|Name|Purpose| +|---|---| +|binary_sensor.NAME_door|open/closed sensor|| +|binary_sensor.NAME_battery|battery good/low| +|sensor.NAME_heartbeat|regular update from device to confirm communication| + +### Device type 'dimmer' entities + +|Name|Purpose| +|---|---| +|light.NAME|lever/paddle dimmer control| + +### Device type 'ezio4o' entities + +|Name|Purpose| +|---|---| +|switch.NAME_relay_1|low-voltage relay 1| +|switch.NAME_relay_2|low-voltage relay 2| +|switch.NAME_relay_3|low-voltage relay 3| +|switch.NAME_relay_4|low-voltage relay 4| + +### Device type 'fan_linc' entities + +|Name|Purpose| +|---|---| +|fan.NAME_fan|multi-speed fan controller| +|light.NAME|dimmable light controller| + +### Device type 'hidden_door' entities + +|Name|Purpose| +|---|---| +|binary_sensor.NAME_door|open/closed sensor| +|binary_sensor.NAME_battery|battery good/low| +|sensor.NAME_heartbeat|regular update from device to confirm communication| +|sensor.NAME_voltage|battery voltage| + +### Device type 'io_linc' entities + +|Name|Purpose| +|---|---| +|switch.NAME_relay|low-voltage relay| +|binary_sensor.NAME_sensor|contact closure input| + +### Device type 'keypad_linc' entities + +Note that Home Assistant control of buttons 2-8 will +only turn the button LEDs on and off; it will not +trigger the responders that have been linked (in a scene) +to the buttons. + +|Name|Purpose| +|---|---| +|light.NAME_btn_1|'On'on 6-button, 'A' on 8-button| +|switch.NAME_btn_2|not usable on 6-button, 'B' on 8-button| +|switch.NAME_btn_3|'A' on 6-button, 'C' on 8-button| +|switch.NAME_btn_4|'B' on 6-button, 'D' on 8-button| +|switch.NAME_btn_5|'C' on 6-button, 'E' on 8-button| +|switch.NAME_btn_6|'D' on 6-button, 'F' on 8-button| +|switch.NAME_btn_7|not usable on 6-button, 'G' on 8-button| +|switch.NAME_btn_8|not usable on 6-button, 'H' on 8-button| +|switch.NAME_btn_9|not currently usable in Insteon-MQTT| + +### Device type 'keypad_linc_sw' entities + +Note that Home Assistant control of buttons 2-8 will +only turn the button LEDs on and off; it will not +trigger the responders that have been linked (in a scene) +to the buttons. + +|Name|Purpose| +|---|---| +|light.NAME_btn_1|'On'on 6-button, 'A' on 8-button| +|switch.NAME_btn_2|not usable on 6-button, 'B' on 8-button| +|switch.NAME_btn_3|'A' on 6-button, 'C' on 8-button| +|switch.NAME_btn_4|'B' on 6-button, 'D' on 8-button| +|switch.NAME_btn_5|'C' on 6-button, 'E' on 8-button| +|switch.NAME_btn_6|'D' on 6-button, 'F' on 8-button| +|switch.NAME_btn_7|not usable on 6-button, 'G' on 8-button| +|switch.NAME_btn_8|not usable on 6-button, 'H' on 8-button| +|switch.NAME_btn_9|not currently usable in Insteon-MQTT| + +### Device type 'leak' entities + +|Name|Purpose| +|---|---| +|binary_sensor.NAME_leak|wet/dry sensor| +|sensor.NAME_heartbeat|regular update from device to confirm communication| + +### Device type 'mini_remote1' entities + +|Name|Purpose| +|---|---| +|binary_sensor.NAME_btn_1|paddle switch| +|binary_sensor.NAME_btn_2|not usable| +|binary_sensor.NAME_btn_3|not usable| +|binary_sensor.NAME_btn_4|not usable| +|binary_sensor.NAME_btn_5|not usable| +|binary_sensor.NAME_btn_6|not usable| +|binary_sensor.NAME_btn_7|not usable| +|binary_sensor.NAME_btn_8|not usable| +|binary_sensor.NAME_battery|battery good/low| + +### Device type 'mini_remote4' entities + +|Name|Purpose| +|---|---| +|binary_sensor.NAME_btn_1|'a' switch| +|binary_sensor.NAME_btn_2|'b' switch| +|binary_sensor.NAME_btn_3|'c' switch| +|binary_sensor.NAME_btn_4|'d' switch| +|binary_sensor.NAME_btn_5|not usable| +|binary_sensor.NAME_btn_6|not usable| +|binary_sensor.NAME_btn_7|not usable| +|binary_sensor.NAME_btn_8|not usable| +|binary_sensor.NAME_battery|battery good/low| + +### Device type 'mini_remote8' entities + +|Name|Purpose| +|---|---| +|binary_sensor.NAME_btn_1|'a' switch| +|binary_sensor.NAME_btn_2|'b' switch| +|binary_sensor.NAME_btn_3|'c' switch| +|binary_sensor.NAME_btn_4|'d' switch| +|binary_sensor.NAME_btn_5|'e' switch| +|binary_sensor.NAME_btn_6|'f' switch| +|binary_sensor.NAME_btn_7|'g' switch| +|binary_sensor.NAME_btn_8|'h' switch| +|binary_sensor.NAME_battery|battery good/low| + +### Device type 'motion' entities + +|Name|Purpose| +|---|---| +|binary_sensor.NAME_motion|motion sensor| +|binary_sensor.NAME_battery|battery good/low| +|binary_sensor.NAME_dusk|dawn/dusk (light level) sensor| + +### Device type 'smoke_bridge' entities + +|Name|Purpose| +|---|---| +|binary_sensor.NAME_smoke|smoke sensor| +|binary_sensor.NAME_battery|battery good/low| +|binary_sensor.NAME_co|carbon monoxide sensor| +|binary_sensor.NAME_error|operational error| + +### Device type 'switch' entities + +|Name|Purpose| +|---|---| +|switch.NAME|toggle/paddle switch| + +### Device type 'outlet' entities + +|Name|Purpose| +|---|---| +|switch.NAME_top|upper receptacle| +|switch.NAME_bottom|lower receptacle| diff --git a/docs/migrating.md b/docs/migrating.md index a20d88ad..e90bd417 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -1,6 +1,6 @@ # Migrating to Discovery for Installations 0.8.3 and Earlier -Prior to version 1.0.0 InsteonMQTT used a single configuration file. Starting in version 1.0.0, the base configuration settings are contained in a base configuration file that ships with InsteonMQTT. You can view the contents of this file here: [config-base.yaml](https://github.com/TD22057/insteon-mqtt/blob/master/insteon_mqtt/data/config-base.yaml) +Prior to version 1.0.0 InsteonMQTT used a single configuration file. Starting in version 1.0.0, the base configuration settings are contained in a base configuration file that ships with InsteonMQTT. You can view the contents of this file here: [config-base.yaml](../insteon_mqtt/data/config-base.yaml) As described in [configuration](configuration.md), the settings in this base configuration file can be overwritten using your user configuration file. @@ -36,7 +36,7 @@ do in steps, so be sure you have enough time set aside. 2. Make a backup copy of all HomeAssistant configurations that define insteon entities. 3. Rename `config-yaml.default` to `config.yaml`. -4. Follow the insstructions [Configuration Instructions](https://github.com/TD22057/insteon-mqtt/blob/master/docs/configuration.md) copying the details of your modem, devices, and mqtt broker from your backup file. +4. Follow the insstructions [Configuration Instructions](configuration.md) copying the details of your modem, devices, and mqtt broker from your backup file. 5.Remove or comment out the insteon entities in your HomeAssistant configuration. 6. Restart HomeAssistant (your front end will likely be filled with yellow triangles). diff --git a/hassio/entrypoint.sh b/hassio/entrypoint.sh index 8c047ba3..f3fe2f63 100755 --- a/hassio/entrypoint.sh +++ b/hassio/entrypoint.sh @@ -10,7 +10,7 @@ if [ ! -f /config/insteon-mqtt/config.yaml ]; then /bin/cp /config/insteon-mqtt/config.yaml.default /config/insteon-mqtt/config.yaml sed -i "s/#storage: 'data'/storage: '\/config\/insteon-mqtt\/data'/" /config/insteon-mqtt/config.yaml sed -i "s/#file: \/var\/log\/insteon_mqtt.log/file: \/config\/insteon-mqtt\/insteon_mqtt.log/" /config/insteon-mqtt/config.yaml - echo "Please edit the file /config/insteon_mqtt/config.yaml" + echo "Please edit the file /config/insteon-mqtt/config.yaml" echo "Then you can start InsteonMQTT." else python3 /opt/insteon-mqtt/hassio/start.py /config/insteon-mqtt/config.yaml start diff --git a/hassio/webcli/app.py b/hassio/webcli/app.py index 94fe32fd..61402257 100644 --- a/hassio/webcli/app.py +++ b/hassio/webcli/app.py @@ -106,4 +106,4 @@ def handle_estop(message): app.config['cmd'] = [] def start_webcli(): - socketio.run(app, host='0.0.0.0', port='8099') + socketio.run(app, host='0.0.0.0', port='8099', allow_unsafe_werkzeug=True) diff --git a/insteon_mqtt/const.py b/insteon_mqtt/const.py index 6d9aaf20..7fc730cd 100644 --- a/insteon_mqtt/const.py +++ b/insteon_mqtt/const.py @@ -11,6 +11,6 @@ variable throughout the code without causing a cyclic import """ -__version__ = "1.1.1" +__version__ = "1.2.0" #=========================================================================== diff --git a/insteon_mqtt/data/config-base.yaml b/insteon_mqtt/data/config-base.yaml index 7dfeba08..c067a3d2 100644 --- a/insteon_mqtt/data/config-base.yaml +++ b/insteon_mqtt/data/config-base.yaml @@ -241,23 +241,21 @@ mqtt: # my experience that it is required. # This device section describes the parent device that all sub-entities are # grouped under - device_info_template: |- - { - "ids": "{{address}}", - "mf": "Insteon", - "mdl": "{%- if model_number != 'Unknown' -%} - {{model_number}} - {{model_description}} - {%- elif dev_cat_name != 'Unknown' -%} - {{dev_cat_name}} - 0x{{'%0x' % sub_cat|int }} - {%- elif dev_cat == 0 and sub_cat == 0 -%} - No Info - {%- else -%} - 0x{{'%0x' % dev_cat|int }} - 0x{{'%0x' % sub_cat|int }} - {%- endif -%}", - "sw": "0x{{'%0x' % firmware|int }} - {{engine}}", - "name": "{{name_user_case}}", - "via_device": "{{modem_addr}}" - } + device_info_template: + ids: "{{address}}" + mf: "Insteon" + mdl: "{%- if model_number != 'Unknown' -%} + {{model_number}} - {{model_description}} + {%- elif dev_cat_name != 'Unknown' -%} + {{dev_cat_name}} - 0x{{'%0x' % sub_cat|int }} + {%- elif dev_cat == 0 and sub_cat == 0 -%} + No Info + {%- else -%} + 0x{{'%0x' % dev_cat|int }} - 0x{{'%0x' % sub_cat|int }} + {%- endif -%}" + sw: "0x{{'%0x' % firmware|int }} - {{engine}}" + name: "{{name_user_case}}" + via_device: "{{modem_addr}}" modem: # Trigger modem virtual scenes. Modem scenes are where the modem is a @@ -297,21 +295,20 @@ mqtt: # listed as a controller. Run `refresh modem` if the entities list # appears incomplete to you. discovery_entities: - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_{{scene}}", - "name": "{%- if scene_name != "" -%} - {{scene_name}} - {%- else -%} - Modem Scene {{scene}} - {%- endif -%}", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{scene_topic}}", - "device": {{device_info_template}}, - "payload_on": "{\"state\": \"on\", \"group\": \"{{scene}}\"}", - "payload_off": "{\"state\": \"off\", \"group\": \"{{scene}}\"}" - } + scene: + component: 'switch' + config: + uniq_id: "{{address}}_{{scene}}" + name: "{%- if scene_name != '' -%} + {{scene_name}} + {%- else -%} + Modem Scene {{scene}} + {%- endif -%}" + avty_t: "{{availability_topic}}" + cmd_t: "{{scene_topic}}" + device: "{{device_info}}" + payload_on: "{\"state\": \"on\", \"group\": \"{{scene}}\"}" + payload_off: "{\"state\": \"off\", \"group\": \"{{scene}}\"}" switch: #------------------------------------------------------------------------ @@ -378,21 +375,20 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_switch", - "name": "{{name_user_case}}", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{on_off_topic}}", - "stat_t": "{{state_topic}}", - "device": {{device_info_template}}, - "json_attr_t": "{{state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } + switch: + component: 'switch' + config: + uniq_id: "{{address}}_switch" + name: "" + avty_t: "{{availability_topic}}" + cmd_t: "{{on_off_topic}}" + stat_t: "{{state_topic}}" + device: "{{device_info}}" + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" dimmer: #------------------------------------------------------------------------ @@ -495,22 +491,21 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'light' - config: |- - { - "uniq_id": "{{address}}_light", - "name": "{{name_user_case}}", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{level_topic}}", - "stat_t": "{{state_topic}}", - "brightness": true, - "schema": "json", - "device": {{device_info_template}}, - "json_attr_t": "{{state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}" - } + dimmer: + component: 'light' + config: + uniq_id: "{{address}}_light" + name: "" + avty_t: "{{availability_topic}}" + cmd_t: "{{level_topic}}" + stat_t: "{{state_topic}}" + brightness: true + schema: "json" + device: "{{device_info}}" + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" battery_sensor: #------------------------------------------------------------------------ @@ -566,50 +561,47 @@ mqtt: # extend this class (motion, hidden_door, leak, and remote) all have their # own discovery_entities discovery_entities: - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_door", - "name": "{{name_user_case}} door", - "stat_t": "{{state_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "door", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_battery", - "name": "{{name_user_case}} battery", - "stat_t": "{{low_battery_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "battery", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{low_battery_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}} } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'sensor' - config: |- - { - "uniq_id": "{{address}}_heartbeat", - "name": "{{name_user_case}} heartbeat", - "stat_t": "{{heartbeat_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "timestamp", - "device": {{device_info_template}}, - "force_update": true, - "val_tpl": "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}" - } + door: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_door" + name: "" + stat_t: "{{state_topic}}" + avty_t: "{{availability_topic}}" + device_class: "door" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + battery: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_battery" + name: "battery" + stat_t: "{{low_battery_topic}}" + avty_t: "{{availability_topic}}" + device_class: "battery" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{low_battery_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + heartbeat: + component: 'sensor' + config: + uniq_id: "{{address}}_heartbeat" + name: "heartbeat" + stat_t: "{{heartbeat_topic}}" + avty_t: "{{availability_topic}}" + device_class: "timestamp" + device: "{{device_info}}" + force_update: true + val_tpl: "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}" motion: #------------------------------------------------------------------------ @@ -638,54 +630,51 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_motion", - "name": "{{name_user_case}} motion", - "stat_t": "{{state_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "motion", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_battery", - "name": "{{name_user_case}} battery", - "stat_t": "{{low_battery_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "battery", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{low_battery_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}} } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_dusk", - "name": "{{name_user_case}} dusk", - "stat_t": "{{dawn_dusk_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "light", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{dawn_dusk_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}} } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } + motion: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_motion" + name: "" + stat_t: "{{state_topic}}" + avty_t: "{{availability_topic}}" + device_class: "motion" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + battery: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_battery" + name: "battery" + stat_t: "{{low_battery_topic}}" + avty_t: "{{availability_topic}}" + device_class: "battery" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{low_battery_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + dusk: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_dusk" + name: "dusk" + stat_t: "{{dawn_dusk_topic}}" + avty_t: "{{availability_topic}}" + device_class: "light" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{dawn_dusk_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" hidden_door: #------------------------------------------------------------------------ @@ -712,66 +701,62 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_door", - "name": "{{name_user_case}} door", - "stat_t": "{{state_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "door", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_battery", - "name": "{{name_user_case}} battery", - "stat_t": "{{low_battery_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "battery", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{low_battery_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}} } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'sensor' - config: |- - { - "uniq_id": "{{address}}_heartbeat", - "name": "{{name_user_case}} heartbeat", - "stat_t": "{{heartbeat_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "timestamp", - "device": {{device_info_template}}, - "force_update": true, - "val_tpl": "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}" - } - - component: 'sensor' - config: |- - { - "uniq_id": "{{address}}_voltage", - "name": "{{name_user_case}} voltage", - "stat_t": "{{battery_voltage_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "voltage", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{battery_voltage_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}} } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.voltage}}{% endraw %}" - } + door: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_door" + name: "" + stat_t: "{{state_topic}}" + avty_t: "{{availability_topic}}" + device_class: "door" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + battery: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_battery" + name: "battery" + stat_t: "{{low_battery_topic}}" + avty_t: "{{availability_topic}}" + device_class: "battery" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{low_battery_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + heartbeat: + component: 'sensor' + config: + uniq_id: "{{address}}_heartbeat" + name: "heartbeat" + stat_t: "{{heartbeat_topic}}" + avty_t: "{{availability_topic}}" + device_class: "timestamp" + device: "{{device_info}}" + force_update: true + val_tpl: "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}" + voltage: + component: 'sensor' + config: + uniq_id: "{{address}}_voltage" + name: "voltage" + stat_t: "{{battery_voltage_topic}}" + avty_t: "{{availability_topic}}" + device_class: "voltage" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{battery_voltage_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.voltage}}{% endraw %}" leak: #------------------------------------------------------------------------ @@ -805,33 +790,31 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_wet", - "name": "{{name_user_case}} leak", - "stat_t": "{{wet_dry_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "moisture", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{wet_dry_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}} } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'sensor' - config: |- - { - "uniq_id": "{{address}}_heartbeat", - "name": "{{name_user_case}} heartbeat", - "stat_t": "{{heartbeat_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "timestamp", - "device": {{device_info_template}}, - "val_tpl": "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}" - } + wet: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_wet" + name: "" + stat_t: "{{wet_dry_topic}}" + avty_t: "{{availability_topic}}" + device_class: "moisture" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{wet_dry_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + heartbeat: + component: 'sensor' + config: + uniq_id: "{{address}}_heartbeat" + name: "heartbeat" + stat_t: "{{heartbeat_topic}}" + avty_t: "{{availability_topic}}" + device_class: "timestamp" + device: "{{device_info}}" + val_tpl: "{%- raw -%}{{as_datetime(value|float|timestamp_local).isoformat()|string}}{%- endraw -%}" remote: #------------------------------------------------------------------------ @@ -886,136 +869,133 @@ mqtt: # https://github.com/TD22057/insteon-mqtt/blob/master/docs/discovery.md # for details about how to define a custom class for your devices. discovery_entities: - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_1", - "name": "{{name_user_case}} btn 1", - "stat_t": "{{state_topic_1}}", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{state_topic_1}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_2", - "name": "{{name_user_case}} btn 2", - "stat_t": "{{state_topic_2}}", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{state_topic_2}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_3", - "name": "{{name_user_case}} btn 3", - "stat_t": "{{state_topic_3}}", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{state_topic_3}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_4", - "name": "{{name_user_case}} btn 4", - "stat_t": "{{state_topic_4}}", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{state_topic_4}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_5", - "name": "{{name_user_case}} btn 5", - "stat_t": "{{state_topic_5}}", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{state_topic_5}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_6", - "name": "{{name_user_case}} btn 6", - "stat_t": "{{state_topic_6}}", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_7", - "name": "{{name_user_case}} btn 7", - "stat_t": "{{state_topic_7}}", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_8", - "name": "{{name_user_case}} btn 8", - "stat_t": "{{state_topic_8}}", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_battery", - "name": "{{name_user_case}} battery", - "stat_t": "{{low_battery_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "battery", - "device": {{device_info_template}}, - "force_update": true, - "json_attr_t": "{{low_battery_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}} } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } + button1: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_1" + name: "btn 1" + stat_t: "{{state_topic_1}}" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic_1}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button2: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_2" + name: "btn 2" + stat_t: "{{state_topic_2}}" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic_2}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button3: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_3" + name: "btn 3" + stat_t: "{{state_topic_3}}" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic_3}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button4: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_4" + name: "btn 4" + stat_t: "{{state_topic_4}}" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic_4}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button5: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_5" + name: "btn 5" + stat_t: "{{state_topic_5}}" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic_5}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button6: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_6" + name: "btn 6" + stat_t: "{{state_topic_6}}" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic_6}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button7: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_7" + name: "btn 7" + stat_t: "{{state_topic_7}}" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic_7}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button8: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_8" + name: "btn 8" + stat_t: "{{state_topic_8}}" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{state_topic_8}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + battery: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_battery" + name: "battery" + stat_t: "{{low_battery_topic}}" + avty_t: "{{availability_topic}}" + device_class: "battery" + device: "{{device_info}}" + force_update: true + json_attr_t: "{{low_battery_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" smoke_bridge: #------------------------------------------------------------------------ @@ -1048,46 +1028,42 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_smoke", - "name": "{{name_user_case}} smoke", - "stat_t": "{{smoke_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "smoke", - "device": {{device_info_template}} - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_battery", - "name": "{{name_user_case}} battery", - "stat_t": "{{battery_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "battery", - "device": {{device_info_template}} - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_co", - "name": "{{name_user_case}} co", - "stat_t": "{{co_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "gas", - "device": {{device_info_template}} - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_error", - "name": "{{name_user_case}} error", - "stat_t": "{{error_topic}}", - "avty_t": "{{availability_topic}}", - "device_class": "problem", - "device": {{device_info_template}} - } + smoke: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_smoke" + name: "smoke" + stat_t: "{{smoke_topic}}" + avty_t: "{{availability_topic}}" + device_class: "smoke" + device: "{{device_info}}" + battery: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_battery" + name: "battery" + stat_t: "{{battery_topic}}" + avty_t: "{{availability_topic}}" + device_class: "battery" + device: "{{device_info}}" + co: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_co" + name: "co" + stat_t: "{{co_topic}}" + avty_t: "{{availability_topic}}" + device_class: "gas" + device: "{{device_info}}" + error: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_error" + name: "error" + stat_t: "{{error_topic}}" + avty_t: "{{availability_topic}}" + device_class: "problem" + device: "{{device_info}}" thermostat: #------------------------------------------------------------------------ @@ -1172,34 +1148,32 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'climate' - config: |- - { - "uniq_id": "{{address}}_thermo", - "name": "{{name_user_case}} thermo", - "act_t": "{{status_state_topic}}", - "avty_t": "{{availability_topic}}", - "curr_temp_t": "{{ambient_temp_topic}}", - "curr_temp_tpl": "{% raw %}{{value_json.temp_f}}{% endraw %}", - "device": {{device_info_template}}, - "fan_mode_cmd_t": "{{fan_command_topic}}", - "fan_mode_stat_t": "{{fan_state_topic}}", - "fan_modes": ["auto", "on"], - "hold_stat_t": "{{hold_state_topic}}", - "max_temp": 95, - "min_temp": 45, - "mode_cmd_t": "{{mode_command_topic}}", - "mode_stat_t": "{{mode_state_topic}}", - "modes": ["off", "cool", "heat", "auto"], - "precision": 1.0, - "temp_hi_cmd_t": "{{cool_sp_command_topic}}", - "temp_hi_stat_t": "{{cool_sp_state_topic}}", - "temp_hi_stat_tpl": "{% raw %}{{value_json.temp_f}}{% endraw %}", - "temp_lo_cmd_t": "{{heat_sp_command_topic}}", - "temp_lo_stat_t": "{{heat_sp_state_topic}}", - "temp_lo_stat_tpl": "{% raw %}{{value_json.temp_f}}{% endraw %}", - "temperature_unit": "F" - } + thermo: + component: 'climate' + config: + uniq_id: "{{address}}_thermo" + name: "thermo" + "act_t": "{{status_state_topic}}" + avty_t: "{{availability_topic}}" + "curr_temp_t": "{{ambient_temp_topic}}" + "curr_temp_tpl": "{% raw %}{{value_json.temp_f}}{% endraw %}" + device: "{{device_info}}" + "fan_mode_cmd_t": "{{fan_command_topic}}" + "fan_mode_stat_t": "{{fan_state_topic}}" + "fan_modes": ["auto", "on"] + "max_temp": 95 + "min_temp": 45 + "mode_cmd_t": "{{mode_command_topic}}" + "mode_stat_t": "{{mode_state_topic}}" + "modes": ["off", "cool", "heat", "auto"] + "precision": 1.0 + "temp_hi_cmd_t": "{{cool_sp_command_topic}}" + "temp_hi_stat_t": "{{cool_sp_state_topic}}" + "temp_hi_stat_tpl": "{% raw %}{{value_json.temp_f}}{% endraw %}" + "temp_lo_cmd_t": "{{heat_sp_command_topic}}" + "temp_lo_stat_t": "{{heat_sp_state_topic}}" + "temp_lo_stat_tpl": "{% raw %}{{value_json.temp_f}}{% endraw %}" + "temperature_unit": "F" fan_linc: #------------------------------------------------------------------------ @@ -1276,44 +1250,42 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'fan' - config: |- - { - "uniq_id": "{{address}}_fan", - "name": "{{name_user_case}} fan", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{fan_on_off_topic}}", - "stat_t": "{{fan_state_topic}}", - "stat_val_tpl": "{% raw %}{{value_json.state}}{% endraw %}", - "pct_cmd_t": "{{fan_speed_set_topic}}", - "pct_cmd_tpl": "{% raw %}{% if value < 10 %}off{% elif value < 40 %}low{% elif value < 75 %}medium{% else %}high{% endif %}{% endraw %}", - "pct_stat_t": "{{fan_speed_topic}}", - "pct_val_tpl": "{% raw %}{% if value == 'low' %}33{% elif value == 'medium' %}67{% elif value == 'high' %}100{% else %}0{% endif %}{% endraw %}", - "pr_mode_stat_t": "{{fan_speed_topic}}", - "pr_mode_cmd_t": "{{fan_speed_set_topic}}", - "pr_modes": ["off", "low", "medium", "high"], - "json_attr_t": "{{fan_state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}" - } - - component: 'light' - config: |- - { - "uniq_id": "{{address}}_light", - "name": "{{name_user_case}}", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{level_topic}}", - "stat_t": "{{state_topic}}", - "brightness": true, - "schema": "json", - "device": {{device_info_template}}, - "json_attr_t": "{{state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}" - } + fan: + component: 'fan' + config: + uniq_id: "{{address}}_fan" + name: "fan" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{fan_on_off_topic}}" + stat_t: "{{fan_state_topic}}" + stat_val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + pct_cmd_t: "{{fan_speed_set_topic}}" + pct_cmd_tpl: "{% raw %}{% if value < 10 %}off{% elif value < 40 %}low{% elif value < 75 %}medium{% else %}high{% endif %}{% endraw %}" + pct_stat_t: "{{fan_speed_topic}}" + pct_val_tpl: "{% raw %}{% if value == 'low' %}33{% elif value == 'medium' %}67{% elif value == 'high' %}100{% else %}0{% endif %}{% endraw %}" + pr_mode_stat_t: "{{fan_speed_topic}}" + pr_mode_cmd_t: "{{fan_speed_set_topic}}" + pr_modes: ["off", "low", "medium", "high"] + json_attr_t: "{{fan_state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + light: + component: 'light' + config: + uniq_id: "{{address}}_light" + name: "light" + avty_t: "{{availability_topic}}" + cmd_t: "{{level_topic}}" + stat_t: "{{state_topic}}" + brightness: true + schema: "json" + device: "{{device_info}}" + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" keypad_linc: #------------------------------------------------------------------------ @@ -1470,154 +1442,145 @@ mqtt: # https://github.com/TD22057/insteon-mqtt/blob/master/docs/discovery.md # for details about how to define a custom class for your devices. discovery_entities: - - component: 'light' - config: |- - { - "uniq_id": "{{address}}_1", - "name": "{{name_user_case}} btn 1", - "avty_t": "{{availability_topic}}", - "device": {{device_info_template}}, - "brightness": {{is_dimmable|lower()}}, - "cmd_t": "{%- if is_dimmable -%} - {{dimmer_level_topic}} - {%- else -%} - {{btn_on_off_topic_1}} - {%- endif -%}", - "schema": "json", - "stat_t": "{%- if is_dimmable -%} - {{dimmer_state_topic}} - {%- else -%} - {{btn_state_topic_1}} - {%- endif -%}", - "json_attr_t": "{%- if is_dimmable -%} - {{dimmer_state_topic}} - {%- else -%} - {{btn_state_topic_1}} - {%- endif -%}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_2", - "name": "{{name_user_case}} btn 2", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{btn_on_off_topic_2}}", - "stat_t": "{{btn_state_topic_2}}", - "json_attr_t": "{{btn_state_topic_2}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_3", - "name": "{{name_user_case}} btn 3", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{btn_on_off_topic_3}}", - "stat_t": "{{btn_state_topic_3}}", - "json_attr_t": "{{btn_state_topic_3}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_4", - "name": "{{name_user_case}} btn 4", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{btn_on_off_topic_4}}", - "stat_t": "{{btn_state_topic_4}}", - "json_attr_t": "{{btn_state_topic_4}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_5", - "name": "{{name_user_case}} btn 5", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{btn_on_off_topic_5}}", - "stat_t": "{{btn_state_topic_5}}", - "json_attr_t": "{{btn_state_topic_5}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_6", - "name": "{{name_user_case}} btn 6", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{btn_on_off_topic_6}}", - "stat_t": "{{btn_state_topic_6}}", - "json_attr_t": "{{btn_state_topic_6}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_7", - "name": "{{name_user_case}} btn 7", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{btn_on_off_topic_7}}", - "stat_t": "{{btn_state_topic_7}}", - "json_attr_t": "{{btn_state_topic_7}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_8", - "name": "{{name_user_case}} btn 8", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{btn_on_off_topic_8}}", - "stat_t": "{{btn_state_topic_8}}", - "json_attr_t": "{{btn_state_topic_8}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_9", - "name": "{{name_user_case}} btn 9", - "device": {{device_info_template}}, - "avty_t": "{{availability_topic}}", - "cmd_t": "{{btn_on_off_topic_9}}", - "stat_t": "{btn_state_topic_9}}", - "json_attr_t": "{{btn_state_topic_9}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } + button1: + component: 'light' + config: + uniq_id: "{{address}}_1" + name: "btn 1" + avty_t: "{{availability_topic}}" + device: "{{device_info}}" + brightness: "{{is_dimmable|lower()}}" + cmd_t: "{%- if is_dimmable -%} + {{dimmer_level_topic}} + {%- else -%} + {{btn_on_off_topic_1}} + {%- endif -%}" + schema: "json" + stat_t: "{%- if is_dimmable -%} + {{dimmer_state_topic}} + {%- else -%} + {{btn_state_topic_1}} + {%- endif -%}" + json_attr_t: "{%- if is_dimmable -%} + {{dimmer_state_topic}} + {%- else -%} + {{btn_state_topic_1}} + {%- endif -%}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + button2: + component: 'switch' + config: + uniq_id: "{{address}}_2" + name: "btn 2" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{btn_on_off_topic_2}}" + stat_t: "{{btn_state_topic_2}}" + json_attr_t: "{{btn_state_topic_2}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button3: + component: 'switch' + config: + uniq_id: "{{address}}_3" + name: "btn 3" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{btn_on_off_topic_3}}" + stat_t: "{{btn_state_topic_3}}" + json_attr_t: "{{btn_state_topic_3}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button4: + component: 'switch' + config: + uniq_id: "{{address}}_4" + name: "btn 4" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{btn_on_off_topic_4}}" + stat_t: "{{btn_state_topic_4}}" + json_attr_t: "{{btn_state_topic_4}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button5: + component: 'switch' + config: + uniq_id: "{{address}}_5" + name: "btn 5" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{btn_on_off_topic_5}}" + stat_t: "{{btn_state_topic_5}}" + json_attr_t: "{{btn_state_topic_5}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button6: + component: 'switch' + config: + uniq_id: "{{address}}_6" + name: "btn 6" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{btn_on_off_topic_6}}" + stat_t: "{{btn_state_topic_6}}" + json_attr_t: "{{btn_state_topic_6}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button7: + component: 'switch' + config: + uniq_id: "{{address}}_7" + name: "btn 7" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{btn_on_off_topic_7}}" + stat_t: "{{btn_state_topic_7}}" + json_attr_t: "{{btn_state_topic_7}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button8: + component: 'switch' + config: + uniq_id: "{{address}}_8" + name: "btn 8" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{btn_on_off_topic_8}}" + stat_t: "{{btn_state_topic_8}}" + json_attr_t: "{{btn_state_topic_8}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + button9: + component: 'switch' + config: + uniq_id: "{{address}}_9" + name: "btn 9" + device: "{{device_info}}" + avty_t: "{{availability_topic}}" + cmd_t: "{{btn_on_off_topic_9}}" + stat_t: "{{btn_state_topic_9}}" + json_attr_t: "{{btn_state_topic_9}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" io_linc: #------------------------------------------------------------------------ @@ -1694,33 +1657,31 @@ mqtt: # Discovery Entities - Used as part of HomeAssistant MQTT Discovery discovery_entities: - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_relay", - "name": "{{name_user_case}} relay", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{on_off_topic}}", - "stat_t": "{{relay_state_topic}}", - "device": {{device_info_template}}, - "json_attr_t": "{{state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}" - } - - component: 'binary_sensor' - config: |- - { - "uniq_id": "{{address}}_sensor", - "name": "{{name_user_case}} sensor", - "avty_t": "{{availability_topic}}", - "stat_t": "{{sensor_state_topic}}", - "device": {{device_info_template}}, - "json_attr_t": "{{state_topic}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}" - } + relay: + component: 'switch' + config: + uniq_id: "{{address}}_relay" + name: "relay" + avty_t: "{{availability_topic}}" + cmd_t: "{{on_off_topic}}" + stat_t: "{{relay_state_topic}}" + device: "{{device_info}}" + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + sensor: + component: 'binary_sensor' + config: + uniq_id: "{{address}}_sensor" + name: "sensor" + avty_t: "{{availability_topic}}" + stat_t: "{{sensor_state_topic}}" + device: "{{device_info}}" + json_attr_t: "{{state_topic}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" outlet: #------------------------------------------------------------------------ @@ -1779,35 +1740,33 @@ mqtt: # https://github.com/TD22057/insteon-mqtt/blob/master/docs/discovery.md # for details about how to define a custom class for your devices. discovery_entities: - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_1", - "name": "{{name_user_case}} top", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{on_off_topic_1}}", - "stat_t": "{{state_topic_1}}", - "device": {{device_info_template}}, - "json_attr_t": "{{state_topic_1}}", - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_2", - "name": "{{name_user_case}} bottom", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{on_off_topic_2}}", - "stat_t": "{{state_topic_2}}", - "device": {{device_info_template}}, - "json_attr_tpl": "{%- raw -%} - { \"timestamp\" : {{value_json.timestamp}}, \"mode\" : \"{{value_json.mode}}\", \"reason\" : \"{{value_json.reason}}\" } - {%- endraw -%}", - "val_tpl": "{% raw %}{{value_json.state}}{% endraw %}" - } + top: + component: 'switch' + config: + uniq_id: "{{address}}_1" + name: "top" + avty_t: "{{availability_topic}}" + cmd_t: "{{on_off_topic_1}}" + stat_t: "{{state_topic_1}}" + device: "{{device_info}}" + json_attr_t: "{{state_topic_1}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" + bottom: + component: 'switch' + config: + uniq_id: "{{address}}_2" + name: "bottom" + avty_t: "{{availability_topic}}" + cmd_t: "{{on_off_topic_2}}" + stat_t: "{{state_topic_2}}" + device: "{{device_info}}" + json_attr_tpl: "{%- raw -%} + {\"timestamp\": {{value_json.timestamp}}, \"mode\": \"{{value_json.mode}}\", \"reason\": \"{{value_json.reason}}\"} + {%- endraw -%}" + val_tpl: "{% raw %}{{value_json.state}}{% endraw %}" ezio4o: #------------------------------------------------------------------------ @@ -1861,45 +1820,41 @@ mqtt: # https://github.com/TD22057/insteon-mqtt/blob/master/docs/discovery.md # for details about how to define a custom class for your devices. discovery_entities: - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_1", - "name": "{{name_user_case}} relay 1", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{on_off_topic_1}}", - "stat_t": "{{state_topic_1}}", - "device": {{device_info_template}} - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_2", - "name": "{{name_user_case}} relay 2", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{on_off_topic_2}}", - "stat_t": "{{state_topic_2}}", - "device": {{device_info_template}} - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_3", - "name": "{{name_user_case}} relay 3", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{on_off_topic_3}}", - "stat_t": "{{state_topic_3}}", - "device": {{device_info_template}} - } - - component: 'switch' - config: |- - { - "uniq_id": "{{address}}_4", - "name": "{{name_user_case}} relay 4", - "avty_t": "{{availability_topic}}", - "cmd_t": "{{on_off_topic_4}}", - "stat_t": "{{state_topic_4}}", - "device": {{device_info_template}} - } + relay1: + component: 'switch' + config: + uniq_id: "{{address}}_1" + name: "relay 1" + avty_t: "{{availability_topic}}" + cmd_t: "{{on_off_topic_1}}" + stat_t: "{{state_topic_1}}" + device: "{{device_info}}" + relay2: + component: 'switch' + config: + uniq_id: "{{address}}_2" + name: "relay 2" + avty_t: "{{availability_topic}}" + cmd_t: "{{on_off_topic_2}}" + stat_t: "{{state_topic_2}}" + device: "{{device_info}}" + relay3: + component: 'switch' + config: + uniq_id: "{{address}}_3" + name: "relay 3" + avty_t: "{{availability_topic}}" + cmd_t: "{{on_off_topic_3}}" + stat_t: "{{state_topic_3}}" + device: "{{device_info}}" + relay4: + component: 'switch' + config: + uniq_id: "{{address}}_4" + name: "relay 4" + avty_t: "{{availability_topic}}" + cmd_t: "{{on_off_topic_4}}" + stat_t: "{{state_topic_4}}" + device: "{{device_info}}" #---------------------------------------------------------------- diff --git a/insteon_mqtt/data/config-schema.yaml b/insteon_mqtt/data/config-schema.yaml index 6f2ec03b..d5fd6ffc 100644 --- a/insteon_mqtt/data/config-schema.yaml +++ b/insteon_mqtt/data/config-schema.yaml @@ -113,10 +113,10 @@ insteon: meta: type_error: >- Device entry value should be a name in string form. - # Opt3 Entry with config_extra settings, impossible to validate - # this here, so runtime code does all the testing. + # Opt3 Entry with additional settings, which can only be partially validated + # here, so runtime code will do the remaining validation. # Setting minlength ensures this broad test only applies when - # config_extra keys are present + # additional keys are present - type: dict keysrules: type: ['string', 'integer'] @@ -126,6 +126,69 @@ insteon: meta: minlength_error: Entry did not have any extra config settings type_error: "Entry was not in 'address: name' format" + allow_unknown: True + schema: + discoverable: + type: boolean + discovery_class: + type: string + discovery_override_class: + type: ['string', 'list'] + schema: + type: string + discovery_overrides: &discovery_overrides + nullable: True + type: dict + keysrules: + type: string + meta: + type_error: Discovery override entity key must be a string + schema: + device: &discovery_device + nullable: True + type: dict + schema: + cu: + type: string + cns: + type: string + ids: + type: string + name: + type: string + mf: + type: string + mdl: + type: string + sa: + type: string + sw: + type: string + via_device: + type: string + allow_unknown: + type: dict + schema: + discoverable: + type: boolean + oneof: + # If entity not discoverable, cannot supply 'component' or 'config' overrides + - allowed: [False] + excludes: ['component', 'config'] + - allowed: [True] + component: + type: string + allowed: ['alarm_control_panel', 'binary_sensor', 'camera', + 'cover', 'device_tracker', 'device_trigger', 'fan', + 'climate', 'light', 'lock', 'scene', 'sensor', + 'switch', 'tag', 'vacuum'] + config: + nullable: True + type: dict + keysrules: + type: string + meta: + type_error: Payload override attribute must be a string meta: oneof_error: | This entry does not match a valid device entry format. @@ -139,26 +202,49 @@ insteon: mqtt: type: dict + keysrules: + type: string + meta: + type_error: MQTT entry must be a string allow_unknown: allow_unknown: False ## The only unknown keys are user defined discovery_class settings type: dict schema: + # A definition of a class with its own entities discovery_entities: &discovery_entities - type: list - schema: - type: dict - schema: - component: - type: string - allowed: ['alarm_control_panel', 'binary_sensor', 'camera', - 'cover', 'device_tracker', 'device_trigger', 'fan', - 'climate', 'light', 'lock', 'scene', 'sensor', - 'switch', 'tag', 'vacuum'] - required: True - config: + excludes: ['discovery_overrides'] + oneof: + - type: dict + keysrules: type: string - required: True + forbidden: ['device'] + meta: + type_error: Discovery entity key must be a string + valuesrules: + type: dict + schema: &discovery_entity + component: + type: string + allowed: ['alarm_control_panel', 'binary_sensor', 'camera', + 'cover', 'device_tracker', 'device_trigger', 'fan', + 'climate', 'light', 'lock', 'scene', 'sensor', + 'switch', 'tag', 'vacuum'] + config: + oneof: + - type: string + - type: dict + keysrules: + type: string + meta: + type_error: Payload attribute must be a string + required: True + - type: list + schema: + type: dict + schema: *discovery_entity + # A definition of a class containing overrides for another class + discovery_overrides: *discovery_overrides schema: broker: type: string @@ -214,8 +300,7 @@ mqtt: type: boolean discovery_topic_base: *mqtt_topic discovery_ha_status: *mqtt_topic - device_info_template: - type: string + device_info_template: *discovery_device modem: type: dict allow_unknown: True diff --git a/insteon_mqtt/data/scenes-schema.yaml b/insteon_mqtt/data/scenes-schema.yaml index 199e1b8c..07dd35be 100644 --- a/insteon_mqtt/data/scenes-schema.yaml +++ b/insteon_mqtt/data/scenes-schema.yaml @@ -24,6 +24,7 @@ ## document contents scenes: + nullable: True type: list schema: type: dict diff --git a/insteon_mqtt/device/BatterySensor.py b/insteon_mqtt/device/BatterySensor.py index 02b5c1ab..06825e9e 100644 --- a/insteon_mqtt/device/BatterySensor.py +++ b/insteon_mqtt/device/BatterySensor.py @@ -115,6 +115,17 @@ def send(self, msg, msg_handler, high_priority=False, after=None): if self._awake_time >= (time.time() - 180): super().send(msg, msg_handler, high_priority, after) else: + # Don't let refresh requests pile up while not awake. Better to + # replace old requests with new ones as they come in. + if str(msg) == str(Msg.OutStandard.direct(self.addr, 0x19, 0x00)): + for i in range(len(self._send_queue)): + if str(msg) == str(self._send_queue[i][0]): + LOG.ui("BatterySensor %s - replacing previously-queued" + " refresh request (device not awake)", + self.label) + self._send_queue[i] = [msg, msg_handler, + high_priority, after] + return LOG.ui("BatterySensor %s - queueing msg until awake", self.label) self._send_queue.append([msg, msg_handler, high_priority, after]) diff --git a/insteon_mqtt/device/Motion.py b/insteon_mqtt/device/Motion.py index 4bed65c9..06715e73 100644 --- a/insteon_mqtt/device/Motion.py +++ b/insteon_mqtt/device/Motion.py @@ -205,7 +205,7 @@ def handle_dawn(self, msg): # Send True for dawn, False for dusk. LOG.info("Motion %s broadcast grp: %s cmd %s", self.addr, msg.group, msg.cmd1) - self.signal_dawn.emit(self, msg.cmd1 == Msg.CmdType.ON) + self.signal_dawn.emit(self, msg.cmd1 == Msg.CmdType.OFF) #----------------------------------------------------------------------- def update_flags(self, on_done=None, **kwargs): diff --git a/insteon_mqtt/device/Switch.py b/insteon_mqtt/device/Switch.py index 995237b4..30a9302f 100644 --- a/insteon_mqtt/device/Switch.py +++ b/insteon_mqtt/device/Switch.py @@ -40,3 +40,67 @@ def __init__(self, protocol, modem, address, name=None, config_extra=None): self.group_map.update({0x01: self.handle_on_off}) #----------------------------------------------------------------------- + def group_cmd_on_off(self, entry, is_on): + """Determine if device turns on or off for this Group Command + + For switches, the database entry holds the actual on/off state that + is applied when the ON command is received. + + Args: + entry (DeviceEntry): The local db entry for this group command. + is_on (bool): Whether the command was ON or OFF + Returns: + is_on (bool): The actual is_on value based on DB entry + """ + # For on command, get actual on/off state from the database entry + if is_on: + is_on = bool(entry.data[0]) + return is_on + + #----------------------------------------------------------------------- + def link_data_to_pretty(self, is_controller, data): + """Converts Link Data1-3 to Human Readable Attributes + + This takes a list of the data values 1-3 and returns a dict with + the human readable attibutes as keys and the human readable values + as values. + + Args: + is_controller (bool): True if the device is the controller, false + if it's the responder. + data (list[3]): List of three data values. + + Returns: + list[3]: list, containing a dict of the human readable values + """ + ret = [{'data_1': data[0]}, {'data_2': data[1]}, {'data_3': data[2]}] + if not is_controller: + on = 1 if data[0] else 0 + ret = [{'on_off': on}, + {'data_2': data[1]}, + {'data_3': data[2]}] + return ret + + #----------------------------------------------------------------------- + def link_data_from_pretty(self, is_controller, data): + """Converts Link Data1-3 from Human Readable Attributes + + This takes a dict of the human readable attributes as keys and their + associated values and returns a list of the data1-3 values. + + Args: + is_controller (bool): True if the device is the controller, false + if it's the responder. + data (dict[3]): Dict of three data values. + + Returns: + list[3]: List of Data1-3 values + """ + data_1, data_2, data_3 = super().link_data_from_pretty(is_controller, + data) + if not is_controller: + if 'on_off' in data: + data_1 = 0xFF if data['on_off'] else 0x00 + return [data_1, data_2, data_3] + + #----------------------------------------------------------------------- diff --git a/insteon_mqtt/device/base/ResponderBase.py b/insteon_mqtt/device/base/ResponderBase.py index 542fe0e2..13115d1f 100644 --- a/insteon_mqtt/device/base/ResponderBase.py +++ b/insteon_mqtt/device/base/ResponderBase.py @@ -113,6 +113,12 @@ def on(self, group=0x01, level=None, mode=on_off.Mode.NORMAL, reason="", """ LOG.info("Device %s grp: %s cmd: on %s", self.addr, group, mode) assert group in self.responder_groups + if isinstance(mode, str): + try: + mode = on_off.Mode(mode.lower()) + except: + LOG.error("Invalid mode string '%s'", mode) + mode = on_off.Mode.NORMAL assert isinstance(mode, on_off.Mode) # Send the requested on code value. @@ -150,6 +156,12 @@ def off(self, group=0x01, mode=on_off.Mode.NORMAL, reason="", """ LOG.info("Device %s grp: %s cmd: off %s", self.addr, group, mode) assert group in self.responder_groups + if isinstance(mode, str): + try: + mode = on_off.Mode(mode.lower()) + except: + LOG.error("Invalid mode string '%s'", mode) + mode = on_off.Mode.NORMAL assert isinstance(mode, on_off.Mode) # Send an off or instant off command. @@ -289,6 +301,7 @@ def handle_group_cmd(self, addr, msg): self.label, msg.group, addr) is_on, mode = on_off.Mode.decode(msg.cmd1) level = self.group_cmd_on_level(entry, is_on) + is_on = self.group_cmd_on_off(entry, is_on) self._set_state(group=localGroup, is_on=is_on, level=level, mode=mode, reason=reason) @@ -341,6 +354,21 @@ def group_cmd_on_level(self, entry, is_on): level = None return level + #----------------------------------------------------------------------- + def group_cmd_on_off(self, entry, is_on): + """Determine if device turns on or off for this Group Command + + For example, the database entry holds the actual on/off state that + is applied when the ON command is received by switches. + + Args: + entry (DeviceEntry): The local db entry for this group command. + is_on (bool): Whether the command was ON or OFF + Returns: + is_on (bool): The actual is_on value based on DB entry + """ + return is_on + #----------------------------------------------------------------------- def group_cmd_handle_increment(self, cmd, group, reason): """Process Increment Group Commands @@ -356,7 +384,7 @@ def group_cmd_handle_increment(self, cmd, group, reason): """ # I am not sure I am aware of increment group commands. Is there # some way I can cause one to occur? - pass + pass # pragma: no cover #----------------------------------------------------------------------- def group_cmd_handle_manual(self, manual, group, reason): @@ -372,4 +400,4 @@ def group_cmd_handle_manual(self, manual, group, reason): group (int): The local db entry for this group command. reason (str): Whether the command was ON or OFF """ - pass + pass # pragma: no cover diff --git a/insteon_mqtt/mqtt/Modem.py b/insteon_mqtt/mqtt/Modem.py index 8e313995..04afde78 100644 --- a/insteon_mqtt/mqtt/Modem.py +++ b/insteon_mqtt/mqtt/Modem.py @@ -3,6 +3,7 @@ # MQTT PLM modem device # #=========================================================================== +import json import re from .. import log from . import topic @@ -64,8 +65,8 @@ def load_config(self, config, qos=None): # Loop all of the discovery entities and append them to # self.rendered_topic_map entities = class_config.get('discovery_entities', None) - if entities is None or not isinstance(entities, list): - LOG.error("%s - No discovery_entities defined, or not a list %s", + if entities is None or not isinstance(entities, dict): + LOG.error("%s - No discovery_entities defined, or not a dict %s", self.device.label, entities) return @@ -73,7 +74,7 @@ def load_config(self, config, qos=None): LOG.warning("%s - Modem only uses the first discovery_entity, " "ignoring the rest %s", self.device.label, entities) - entity = entities[0] + entity = list(entities.values())[0] component = entity.get('component', None) if component is None: LOG.error("%s - No component specified in discovery entity %s", @@ -86,6 +87,12 @@ def load_config(self, config, qos=None): self.device.label, entity) return + payload = json.dumps(payload, indent=2) + # replace reference to device_info as string + # with reference as object (remove quotes) + payload = re.sub(r'"{{\s*device_info\s*}}"', '{{device_info}}', + payload) + # Get Unique ID from payload to use in topic unique_id = self._get_unique_id(payload) if unique_id is None: diff --git a/insteon_mqtt/mqtt/Mqtt.py b/insteon_mqtt/mqtt/Mqtt.py index 66fa55b4..9bfd1bc7 100644 --- a/insteon_mqtt/mqtt/Mqtt.py +++ b/insteon_mqtt/mqtt/Mqtt.py @@ -75,7 +75,7 @@ def __init__(self, mqtt_link, modem): self._ha_status_topic = None # The device_info_template - self.device_info_template = "" + self.device_info_template = {} # The availability topic self.availability_topic = "" diff --git a/insteon_mqtt/mqtt/topic/DiscoveryTopic.py b/insteon_mqtt/mqtt/topic/DiscoveryTopic.py index 3ef527ed..fdf1042e 100644 --- a/insteon_mqtt/mqtt/topic/DiscoveryTopic.py +++ b/insteon_mqtt/mqtt/topic/DiscoveryTopic.py @@ -3,8 +3,9 @@ # MQTT Discovery Topic # #=========================================================================== -import re +import copy import json +import re import jinja2 from ... import log from ...catalog import Category @@ -37,6 +38,10 @@ def __init__(self, mqtt, device, **kwargs): # device self.disc_templates = [] + # get a copy of the global device_info_template, so that it can + # be overriden later if needed + self.device_info_template = copy.deepcopy(mqtt.device_info_template) + #----------------------------------------------------------------------- def load_discovery_data(self, config, qos=None): """Load values from a configuration data object. @@ -48,27 +53,83 @@ def load_discovery_data(self, config, qos=None): config (dict): The mqtt section of the config dict. qos (int): The default quality of service level to use. """ - # Skip is discovery not enabled + # Skip if discovery not enabled if not self.mqtt.discovery_enabled: return - # Get the device specific discovery class + # Skip if device should not be discovered + if not self.device.config_extra.get('discoverable', True): + return + + # Get the device specific discovery class(es) disc_class = self.device.config_extra.get('discovery_class', self.default_discovery_cls) - class_config = config.get(disc_class, None) + if isinstance(disc_class, list): + base_class = disc_class[0] + override_classes = disc_class[1:] + else: + base_class = disc_class + override_classes = [] + + dev_over_classes = self.device.config_extra.get( + 'discovery_override_class', + None) + if dev_over_classes: + if isinstance(dev_over_classes, list): + override_classes.extend(dev_over_classes) + else: + override_classes.append(dev_over_classes) + + # handle base_class first, which must provide entities + class_config = config.get(base_class, None) if class_config is None: LOG.error("%s - Unable to find discovery class %s", - self.device.label, disc_class) + self.device.label, base_class) return - - # Loop all of the discovery entities and append them to - # self.rendered_topic_map entities = class_config.get('discovery_entities', None) - if entities is None or not isinstance(entities, list): - LOG.error("%s - No discovery_entities defined, or not a list %s", + if entities is None: + LOG.error("%s - No discovery_entities defined", + self.device.label) + return + if isinstance(entities, list): + # convert old-style (unnamed) entity list to new-style (named) + # names are 'entity' plus the 0-based index in the list + entities = {'entity' + str(i): e for i, e in enumerate(entities)} + elif not isinstance(entities, dict): + LOG.error("%s - discovery_entities must be a mapping - %s", self.device.label, entities) return - for entity in entities: + else: + # a copy of the entities dictionary is needed, so that overrides + # applied later do not modify the original in the base class + entities = copy.deepcopy(entities) + + # handle override classes + for override_class in override_classes: + class_config = config.get(override_class, None) + if class_config is None: + LOG.error("%s - Unable to find discovery class %s", + self.device.label, override_class) + return + disc_overrides = class_config.get('discovery_overrides', None) + if disc_overrides: + if not self._apply_discovery_overrides(entities, + disc_overrides): + return + + # handle overrides from device + disc_overrides = self.device.config_extra.get('discovery_overrides', + None) + if disc_overrides: + if not self._apply_discovery_overrides(entities, disc_overrides): + return + + # Loop all of the discovery entities and append them to + # self.rendered_topic_map + for entity in entities.values(): + if not entity.get('discoverable', True): + continue + component = entity.get('component', None) if component is None: LOG.error("%s - No component specified in discovery entity %s", @@ -81,6 +142,14 @@ def load_discovery_data(self, config, qos=None): self.device.label, entity) continue + # handle dict-style configuration + if isinstance(payload, dict): + payload = json.dumps(payload, indent=2) + # replace reference to device_info as string + # with reference as object (remove quotes) + payload = re.sub(r'"{{\s*device_info\s*}}"', '{{device_info}}', + payload) + # Get Unique ID from payload to use in topic unique_id = self._get_unique_id(payload) if unique_id is None: @@ -135,8 +204,8 @@ def discovery_template_data(self, **kwargs): dev_cat_name = (str) device category name sub_cat = (int) device sub-category modem_addr = (str) hexadecimal address of modem as a string - device_info_template = (jinja template) a template defined in - config.yaml + device_info = (str) a JSON object with info about this device, + produced from its device_info_template <> = (str) topic keys as defined in the config.yaml file are available as variables """ @@ -178,13 +247,16 @@ def discovery_template_data(self, **kwargs): # Finally, render the device_info_template try: device_info_template = jinja2.Template( - self.mqtt.device_info_template + json.dumps(self.device_info_template, indent=2) ) - data['device_info_template'] = device_info_template.render(data) + data['device_info'] = device_info_template.render(data) + # provide a 'device_info_template' alias for configurations + # which use it + data['device_info_template'] = data['device_info'] except jinja2.exceptions.TemplateError as exc: LOG.error("Error rendering device_info_template: %s", exc) LOG.error("Template was: \n%s", - self.mqtt.device_info_template.strip()) + json.dumps(self.device_info_template)) LOG.error("Data passed was: %s", data) return data @@ -255,3 +327,49 @@ def _get_unique_id(self, config): return ret #----------------------------------------------------------------------- + def _apply_discovery_overrides(self, entities, disc_overrides): + for entity_key, overrides in disc_overrides.items(): + # special handling for device-level overrides + if entity_key == 'device': + device = self.device_info_template + if overrides: + device.update(overrides) + # delete any keys with empty string values + keys_to_delete = [] + for key, val in device.items(): + if val == "": + keys_to_delete.append(key) + for key in keys_to_delete: + del device[key] + continue + + if entity_key not in entities: + LOG.error("%s - Entity to override was not found - %s", + self.device.label, entity_key) + return False + + entity = entities[entity_key] + + if not overrides.get('discoverable', True): + entity['discoverable'] = False + continue + + if 'component' in overrides: + entity['component'] = overrides['component'] + + if 'config' in overrides and overrides['config']: + config = entity['config'] + if not isinstance(config, dict): + LOG.error("%s - Config as string cannot be overriden - %s", + self.device.label, entity_key) + return False + config.update(overrides['config']) + # delete any keys with empty string values + keys_to_delete = [] + for key, val in config.items(): + if val == "": + keys_to_delete.append(key) + for key in keys_to_delete: + del config[key] + + return True diff --git a/requirements.txt b/requirements.txt index 7962ce1c..5493d732 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ paho-mqtt>=1.3 pyserial>=3.2 pyyaml>=3 Jinja2>=2.1 -ruamel.yaml>=0.15 +ruamel.yaml>=0.15,<=0.17.17 Flask>=1.1.2 Flask-SocketIO>=4.3.2 requests>=2.18.2 diff --git a/setup.py b/setup.py index 8c4c30b8..f802e8f5 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name = 'insteon-mqtt', - version = '1.1.1', + version = '1.2.0', description = "Insteon <-> MQTT bridge server", long_description = readme, author = "Ted Drain", diff --git a/tests/configs/discovery_schema_class_mixed.yaml b/tests/configs/discovery_schema_class_mixed.yaml new file mode 100644 index 00000000..d0034e65 --- /dev/null +++ b/tests/configs/discovery_schema_class_mixed.yaml @@ -0,0 +1,12 @@ +mqtt: + broker: 127.0.0.1 + port: 1883 + class1: + discovery_overrides: + switch: + config: + item1: 'foo' + discovery_entities: + light: + component: 'light' + config: 'foo' diff --git a/tests/configs/discovery_schema_device_discoverable_overrides.yaml b/tests/configs/discovery_schema_device_discoverable_overrides.yaml new file mode 100644 index 00000000..518c2723 --- /dev/null +++ b/tests/configs/discovery_schema_device_discoverable_overrides.yaml @@ -0,0 +1,9 @@ +insteon: + port: 'foo' + devices: + switch: + - aa.aa.12: 'device 2' + discovery_overrides: + switch: + discoverable: false + component: 'light' diff --git a/tests/configs/discovery_schema_device_unknown.yaml b/tests/configs/discovery_schema_device_unknown.yaml new file mode 100644 index 00000000..ea0c0458 --- /dev/null +++ b/tests/configs/discovery_schema_device_unknown.yaml @@ -0,0 +1,8 @@ +insteon: + port: 'foo' + devices: + switch: + - aa.aa.12: 'device 2' + discovery_overrides: + device: + foo: 'bar' diff --git a/tests/configs/discovery_schema_good.yaml b/tests/configs/discovery_schema_good.yaml new file mode 100644 index 00000000..eb17a1a6 --- /dev/null +++ b/tests/configs/discovery_schema_good.yaml @@ -0,0 +1,134 @@ +# This configuration tests validation of all documented +# combinations of discovery configurations. + +insteon: + port: 'foo' + devices: + switch: + # device not discoverable + - aa.aa.11: 'device 1' + discoverable: false + # entity not discoverable + - aa.aa.12: 'device 2' + discovery_overrides: + switch: + discoverable: false + # component type override + - aa.aa.13: 'device 3' + discovery_overrides: + switch: + component: 'light' + # discovery_override_class (single) + - aa.aa.14: 'device 4' + discovery_override_class: 'class1' + # discovery_override_class (multiple) + - aa.aa.15: 'device 5' + discovery_override_class: + - 'class1' + - 'class2' + # device override (single) + - aa.aa.16: 'device 6' + discovery_overrides: + device: + name: 'foo' + # device override (multiple) + - aa.aa.17: 'device 7' + discovery_overrides: + device: + name: 'foo' + mdl: 'foo' + # entity override (single) + - aa.aa.17: 'device 7' + discovery_overrides: + switch: + config: + item1: 'foo' + # entity override (multiple) + - aa.aa.18: 'device 8' + discovery_overrides: + switch: + config: + item1: 'foo' + item2: 'foo' + # all types of overrides, plus class + - aa.aa.19: 'device 9' + discovery_override_class: 'class1' + discovery_overrides: + device: + name: 'foo' + switch: + component: 'light' + config: + item1: 'foo' + # empty override map + - aa.aa.20: 'device 10' + discovery_overrides: + # empty device override map + - aa.aa.22: 'device 12' + discovery_overrides: + device: + # empty config override map + - aa.aa.23: 'device 13' + discovery_overrides: + switch: + config: + +mqtt: + broker: 127.0.0.1 + port: 1883 + # class with entity override (single) + class1: + discovery_overrides: + switch: + config: + item1: 'foo' + # class with entity override (multiple) + class2: + discovery_overrides: + switch: + config: + item1: 'foo' + item2: 'foo' + # class with device override (single) + class3: + discovery_overrides: + device: + name: 'foo' + # class with device override (multiple) + class4: + discovery_overrides: + device: + name: 'foo' + mdl: 'foo' + # class with entity not discoverable + class5: + discovery_overrides: + switch: + discoverable: false + # class with component type override + class6: + discovery_overrides: + switch: + component: 'light' + # class with all types of overrides + class7: + discovery_overrides: + device: + name: 'foo' + switch: + component: 'light' + config: + item1: 'foo' + # class with entities (config as string) + class8: + discovery_entities: + light: + component: 'light' + config: 'foo' + # class with entities (config as dict) + class9: + discovery_entities: + light: + component: 'light' + config: + foo: 'bar' diff --git a/tests/configs/empty.yaml b/tests/configs/empty.yaml new file mode 100644 index 00000000..e69de29b diff --git a/tests/configs/scenes_empty.yaml b/tests/configs/scenes_empty.yaml new file mode 100644 index 00000000..e2bafd85 --- /dev/null +++ b/tests/configs/scenes_empty.yaml @@ -0,0 +1,7 @@ +insteon: + port: '/dev/insteon' + scenes: !rel_path empty.yaml + +mqtt: + broker: 127.0.0.1 + port: 1883 diff --git a/tests/device/base/test_ResponderBase.py b/tests/device/base/test_ResponderBase.py new file mode 100644 index 00000000..20e28843 --- /dev/null +++ b/tests/device/base/test_ResponderBase.py @@ -0,0 +1,100 @@ +#=========================================================================== +# +# Tests for: insteont_mqtt/device/base/ResponderBase.py +# +# pylint: disable=W0621,W0212, +# +#=========================================================================== +import logging +from pathlib import Path +# from pprint import pprint +from unittest import mock +from unittest.mock import call +import pytest +import insteon_mqtt as IM +from insteon_mqtt.device.base.ResponderBase import ResponderBase +from insteon_mqtt.device.base import Base +import insteon_mqtt.message as Msg +import insteon_mqtt.handler as Handler +import insteon_mqtt.on_off as on_off +import helpers as H + +@pytest.fixture +def test_device(tmpdir): + ''' + Returns a generically configured device for testing + ''' + protocol = H.main.MockProtocol() + modem = H.main.MockModem(tmpdir) + modem.db = IM.db.Modem(None, modem) + modem.scenes = IM.Scenes.SceneManager(modem, None) + addr = IM.Address(0x01, 0x02, 0x03) + device = ResponderBase(protocol, modem, addr) + return device + +class Test_ResponderBase_Cmds(): + ## On Command + @pytest.mark.parametrize("mode_arg,cmd1", [ + (None, 0x11), # mode not set + (on_off.Mode.INSTANT, 0x21), # mode as enum + ("instant", 0x21), # mode as str + ("bad_mode", 0x11), # bad mode + ]) + def test_on(self, test_device, mode_arg, cmd1): + with mock.patch.object(Base, 'send') as mocked: + if mode_arg is not None: + test_device.on(mode=mode_arg) + else: + test_device.on() + assert mocked.call_count == 1 + call_args = mocked.call_args_list + assert isinstance(call_args[0].args[0], Msg.OutStandard) + assert call_args[0].args[0].cmd1 == cmd1 + assert call_args[0].args[0].cmd2 == 0xFF + assert isinstance(call_args[0].args[1], Handler.StandardCmd) + + ## Off Command + @pytest.mark.parametrize("mode_arg,cmd1", [ + (None, 0x13), # mode not set + (on_off.Mode.INSTANT, 0x21), # mode as enum + ("instant", 0x21), # mode as str + ("bad_mode", 0x13), # bad mode + ]) + def test_off(self, test_device, mode_arg, cmd1): + with mock.patch.object(Base, 'send') as mocked: + if mode_arg is not None: + test_device.off(mode=mode_arg) + else: + test_device.off() + assert mocked.call_count == 1 + call_args = mocked.call_args_list + assert isinstance(call_args[0].args[0], Msg.OutStandard) + assert call_args[0].args[0].cmd1 == cmd1 + assert call_args[0].args[0].cmd2 == 0x00 + assert isinstance(call_args[0].args[1], Handler.StandardCmd) + + ## Set Command + @pytest.mark.parametrize("mode_arg,is_on,level,cmd1,cmd2", [ + (None, None, None, 0x13, 0x00), + (None, True, None, 0x11, 0xFF), + (None, None, 0x50, 0x11, 0xFF), + (on_off.Mode.INSTANT, None, None, 0x21, 0x00), + ('instant', None, None, 0x21, 0x00), + ], + ids=['mode not set', 'testB id', 'level is set', 'mode is enum', 'mode is str']) + def test_set(self, test_device, mode_arg, is_on, level, cmd1, cmd2): + kwargs = {} + with mock.patch.object(Base, 'send') as mocked: + if mode_arg is not None: + kwargs['mode'] = mode_arg + if is_on is not None: + kwargs['is_on'] = is_on + if level is not None: + kwargs['level'] = level + test_device.set(**kwargs) + assert mocked.call_count == 1 + call_args = mocked.call_args_list + assert isinstance(call_args[0].args[0], Msg.OutStandard) + assert call_args[0].args[0].cmd1 == cmd1 + assert call_args[0].args[0].cmd2 == cmd2 + assert isinstance(call_args[0].args[1], Handler.StandardCmd) \ No newline at end of file diff --git a/tests/device/test_BatterySensorDev.py b/tests/device/test_BatterySensorDev.py index ba5a2360..80e1f2fb 100644 --- a/tests/device/test_BatterySensorDev.py +++ b/tests/device/test_BatterySensorDev.py @@ -133,3 +133,22 @@ def on_done(*args): assert len(test_device.protocol.sent) == 1 assert len(test_device._send_queue) == 0 assert msg_handler._num_retry > 0 + + def test_queued_refresh_replace(self, test_device): + # Queue multiple refresh commands + msg1 = Msg.OutStandard.direct(test_device.addr, 0x19, 0x00) + msg2 = Msg.OutStandard.direct(test_device.addr, 0x19, 0x00) + msg3 = Msg.OutStandard.direct(test_device.addr, 0x19, 0x00) + msg_handler1 = IM.handler.StandardCmd(msg1, None, None) + msg_handler2 = IM.handler.StandardCmd(msg2, None, None) + msg_handler3 = IM.handler.StandardCmd(msg3, None, None) + test_device.send(msg1, msg_handler1) + test_device.send(msg2, msg_handler2) + test_device.send(msg3, msg_handler3) + # Confirm that only the last command is queued + assert len(test_device._send_queue) == 1 + m, h, p, a = test_device._send_queue[0] + assert m != msg1 + assert m == msg3 + assert h != msg_handler1 + assert h == msg_handler3 diff --git a/tests/device/test_MotionDev.py b/tests/device/test_MotionDev.py index 4646cb06..241d865a 100644 --- a/tests/device/test_MotionDev.py +++ b/tests/device/test_MotionDev.py @@ -64,8 +64,8 @@ def test_handle_broadcast_state(self, test_device, group_num, cmd1, cmd2, expect mocked.assert_not_called() @pytest.mark.parametrize("group_num,cmd1,cmd2,expected", [ - (0x02,Msg.CmdType.ON, 0x00,[True]), - (0x02,Msg.CmdType.OFF, 0x00, [False]), + (0x02,Msg.CmdType.ON, 0x00,[False]), + (0x02,Msg.CmdType.OFF, 0x00, [True]), (0x03,Msg.CmdType.ON, 0x00,[True]), (0x03,Msg.CmdType.OFF, 0x00, [False]), (0x04,Msg.CmdType.ON, 0x00,[True]), diff --git a/tests/device/test_SwitchDev.py b/tests/device/test_SwitchDev.py index 1cc41abd..188dba9a 100644 --- a/tests/device/test_SwitchDev.py +++ b/tests/device/test_SwitchDev.py @@ -82,6 +82,56 @@ def test_handle_on_off_manual(self, test_device): mode=IM.on_off.Mode.MANUAL, reason='device')] mocked.assert_has_calls(calls) + @pytest.mark.parametrize("cmd1, entry_d1, expected", [ + (0x11, None, None), + (0x11, 0xFF, True), + (0x11, 0x00, False), + (0x13, None, None), + (0x13, 0xFF, False), + (0x13, 0x00, False), + ]) + def test_handle_group_cmd(self, test_device, cmd1, entry_d1, expected): + with mock.patch.object(IM.Signal, 'emit'): + # Build the msg to send to the handler + to_addr = test_device.addr + from_addr = IM.Address(0x04, 0x05, 0x06) + flags = IM.message.Flags(IM.message.Flags.Type.ALL_LINK_CLEANUP, + False) + msg = IM.message.InpStandard(from_addr, to_addr, flags, cmd1, 0x01) + # If db entry is requested, build and add the entry to the dev db + if entry_d1 is not None: + db_flags = IM.message.DbFlags(True, False, True) + entry = IM.db.DeviceEntry(from_addr, 0x01, 0xFFFF, db_flags, + bytes([entry_d1, 0x00, 0x00])) + test_device.db.add_entry(entry) + # send the message to the handler + test_device.handle_group_cmd(from_addr, msg) + # Test the responses received + calls = IM.Signal.emit.call_args_list + if expected is not None: + print(calls) + assert calls[0][1]['is_on'] == expected + assert calls[0][1]['button'] == 1 + assert IM.Signal.emit.call_count == 1 + else: + assert IM.Signal.emit.call_count == 0 + + @pytest.mark.parametrize("data_1, pretty_data_1, name, is_controller", [ + (0x00, 0, 'on_off', False), + (0xFF, 1, 'on_off', False), + (0xFF, 0XFF, 'data_1', True), + ]) + def test_link_data(self, test_device, data_1, pretty_data_1, name, + is_controller): + pretty = test_device.link_data_to_pretty(is_controller, + [data_1, 0x00, 0x00]) + assert pretty[0][name] == pretty_data_1 + ugly = test_device.link_data_from_pretty(is_controller, + {name: pretty_data_1, + 'data_2': 0x00, + 'data_3': 0x00}) + assert ugly[0] == data_1 + def test_set_backlight(self, test_device): test_device.set_backlight(backlight=0) assert len(test_device.protocol.sent) == 1 diff --git a/tests/mqtt/test_Modem.py b/tests/mqtt/test_Modem.py index 05946ca3..0e534e02 100644 --- a/tests/mqtt/test_Modem.py +++ b/tests/mqtt/test_Modem.py @@ -129,7 +129,7 @@ def test_discovery_publish(self, setup): Modem Scene {{scene}} {%- endif -%}", "cmd_t": "{{scene_topic}}", - "device": {{device_info_template}}, + "device": {{device_info}}, "payload_on": "{\"cmd\": \"on\", \"group\": \"{{scene}}\"}", "payload_off": "{\"cmd\": \"off\", \"group\": \"{{scene}}\"}" } @@ -161,7 +161,8 @@ def test_discovery_publish(self, setup): 'availability_topic': '', 'dev_cat': 0, 'dev_cat_name': 'Unknown', - 'device_info_template': '', + 'device_info': '{}', + 'device_info_template': '{}', 'engine': 'Unknown', 'firmware': 0, 'model_description': 'Unknown', diff --git a/tests/mqtt/test_ThermostatMqtt.py b/tests/mqtt/test_ThermostatMqtt.py index 48adb9b8..cce23ecd 100644 --- a/tests/mqtt/test_ThermostatMqtt.py +++ b/tests/mqtt/test_ThermostatMqtt.py @@ -219,6 +219,7 @@ def __init__(self): self.last_payload = None self.last_topic = None self.mode_command = None + self.device_info_template = {} def publish(self, topic, payload, qos=None, retain=None): self.last_topic = topic diff --git a/tests/mqtt/topic/test_DiscoveryTopic.py b/tests/mqtt/topic/test_DiscoveryTopic.py index a8935ab9..1fb9c83c 100644 --- a/tests/mqtt/topic/test_DiscoveryTopic.py +++ b/tests/mqtt/topic/test_DiscoveryTopic.py @@ -4,14 +4,13 @@ # # pylint: disable=redefined-outer-name #=========================================================================== +import json from unittest import mock import pytest import insteon_mqtt as IM import helpers as H -# Create the base mqtt object -@pytest.fixture -def discovery(mock_paho_mqtt, tmpdir): +def fixture_setup(tmpdir): link = IM.network.Mqtt() mqttModem = H.mqtt.MockModem() mqtt = IM.mqtt.Mqtt(link, mqttModem) @@ -21,93 +20,359 @@ def discovery(mock_paho_mqtt, tmpdir): protocol = H.main.MockProtocol() modem = H.main.MockModem(tmpdir) addr = IM.Address(0x11, 0x22, 0x33) + + return mqtt, protocol, modem, addr + +# Create a base object of 'switch' type +@pytest.fixture +def discovery_switch(mock_paho_mqtt, tmpdir): + mqtt, protocol, modem, addr = fixture_setup(tmpdir) + device = IM.device.Switch(protocol, modem, addr) - discovery = IM.mqtt.topic.DiscoveryTopic(mqtt, device) - return discovery + return IM.mqtt.topic.DiscoveryTopic(mqtt, device) + +# Create a base object of 'fan_linc' type +@pytest.fixture +def discovery_fan(mock_paho_mqtt, tmpdir): + mqtt, protocol, modem, addr = fixture_setup(tmpdir) + + device = IM.device.FanLinc(protocol, modem, addr) + + return IM.mqtt.topic.DiscoveryTopic(mqtt, device) #=========================================================================== class Test_DiscoveryTopic: #----------------------------------------------------------------------- - def test_load_discovery_data(self, discovery, caplog): + def test_load_discovery_data(self, discovery_switch, caplog): # This also fully tests _get_unique_id() - discovery.mqtt.discovery_enabled = True + discovery_switch.mqtt.discovery_enabled = True # test lack of discovery class config = {} - discovery.load_discovery_data(config) + discovery_switch.load_discovery_data(config) assert 'Unable to find discovery class' in caplog.text caplog.clear() + # request fake class to be used for remaining tests + discovery_switch.device.config_extra['discovery_class'] = 'fake_dev' + # test lack of entities defined config['fake_dev'] = {} - discovery.device.config_extra['discovery_class'] = 'fake_dev' - discovery.load_discovery_data(config) + discovery_switch.load_discovery_data(config) assert 'No discovery_entities defined' in caplog.text caplog.clear() # test lack of component - config['fake_dev'] = {'discovery_entities': [{}]} - discovery.load_discovery_data(config) + config['fake_dev'] = {'discovery_entities': { + 'test': {}, + }} + discovery_switch.load_discovery_data(config) assert 'No component specified in discovery entity' in caplog.text caplog.clear() # Override data at this point - discovery.discovery_template_data = mock.Mock(return_value={}) + discovery_switch.discovery_template_data = mock.Mock(return_value={}) # test lack of unique id - config['fake_dev'] = {'discovery_entities': [{ - "component": "switch", - "config": "{}" - }]} - discovery.load_discovery_data(config) + config['fake_dev'] = {'discovery_entities': { + 'test': { + "component": "switch", + "config": {}, + }, + }} + discovery_switch.load_discovery_data(config) assert 'Error getting unique_id, skipping entry' in caplog.text caplog.clear() # test with unique_id - config['fake_dev'] = {'discovery_entities': [{ - "component": "switch", - "config": '{"unique_id": "unique"}' - }]} - discovery.load_discovery_data(config) + config['fake_dev'] = {'discovery_entities': { + 'test': { + "component": "switch", + "config": {"unique_id": "unique"}, + }, + }} + discovery_switch.load_discovery_data(config) expected_topic = "homeassistant/switch/11_22_33/unique/config" - assert discovery.disc_templates[0].topic_str == expected_topic - discovery.disc_templates = [] + assert discovery_switch.disc_templates[0].topic_str == expected_topic + discovery_switch.disc_templates = [] # test with uniq_id - config['fake_dev'] = {'discovery_entities': [{ - "component": "switch", - "config": '{"uniq_id": "unique2"}' - }]} - discovery.load_discovery_data(config) + config['fake_dev'] = {'discovery_entities': { + 'test': { + "component": "switch", + "config": {"uniq_id": "unique2"}, + }, + }} + discovery_switch.load_discovery_data(config) expected_topic = "homeassistant/switch/11_22_33/unique2/config" - assert discovery.disc_templates[0].topic_str == expected_topic - discovery.disc_templates = [] + assert discovery_switch.disc_templates[0].topic_str == expected_topic + discovery_switch.disc_templates = [] - # test bad json + # test old-style (unnamed) entities config['fake_dev'] = {'discovery_entities': [{ "component": "switch", - "config": "{'no_single': 'quotes'}" + "config": {"unique_id": "unique"} }]} - discovery.load_discovery_data(config) - expected_topic = "homeassistant/switch/11_22_33/unique2/config" + discovery_switch.load_discovery_data(config) + expected_topic = "homeassistant/switch/11_22_33/unique/config" + assert discovery_switch.disc_templates[0].topic_str == expected_topic + discovery_switch.disc_templates = [] + + # test bad json + config['fake_dev'] = {'discovery_entities': { + 'test': { + "component": "switch", + "config": "{'no_single': 'quotes'}", + }, + }} + discovery_switch.load_discovery_data(config) assert 'Error parsing config as json' in caplog.text caplog.clear() # test bad template - config['fake_dev'] = {'discovery_entities': [{ - "component": "switch", - "config": "{% if bad_format = 1 %}" - }]} - discovery.load_discovery_data(config) - expected_topic = "homeassistant/switch/11_22_33/unique2/config" + config['fake_dev'] = {'discovery_entities': { + 'test': { + "component": "switch", + "config": "{% if bad_format = 1 %}", + }, + }} + discovery_switch.load_discovery_data(config) assert 'Error rendering config template' in caplog.text caplog.clear() + # test discovery suppression (entire device) + discovery_switch.device.config_extra['discoverable'] = False + config['fake_dev'] = {'discovery_entities': { + 'test': { + "component": "switch", + "config": {"unique_id": "unique"}, + }, + }} + discovery_switch.load_discovery_data(config) + assert len(discovery_switch.disc_templates) == 0 + del discovery_switch.device.config_extra['discoverable'] + #----------------------------------------------------------------------- - def test_template_data(self, discovery, caplog): + def test_load_device_discovery_overrides(self, discovery_fan, caplog): + discovery_fan.mqtt.discovery_enabled = True + # build and request fake class to be used for tests + config = {} + config['fake_dev'] = {'discovery_entities': { + 'fake': { + "component": "fan", + "config": { + "unique_id": "unique", + "icon": "fake", + }, + }, + }} + discovery_fan.device.config_extra['discovery_class'] = 'fake_dev' + # Override data from this point + discovery_fan.discovery_template_data = mock.Mock(return_value={}) + + # test empty override dict + discovery_fan.device.config_extra['discovery_overrides'] = {} + discovery_fan.load_discovery_data(config) + assert len(discovery_fan.disc_templates) == 1 + discovery_fan.disc_templates = [] + + # test empty device override dict + discovery_fan.device.config_extra['discovery_overrides'] = { 'device': {} } + discovery_fan.load_discovery_data(config) + assert len(discovery_fan.disc_templates) == 1 + discovery_fan.disc_templates = [] + + # test empty config override dict + discovery_fan.device.config_extra['discovery_overrides'] = { 'fake': { + "config": {}, + }} + discovery_fan.load_discovery_data(config) + assert len(discovery_fan.disc_templates) == 1 + discovery_fan.disc_templates = [] + + # test for non-matching entity name + discovery_fan.device.config_extra['discovery_overrides'] = { 'fakefail': { + }} + discovery_fan.load_discovery_data(config) + assert 'Entity to override was not found' in caplog.text + caplog.clear() + + # test for suppressing entity + discovery_fan.device.config_extra['discovery_overrides'] = { 'fake': { + "discoverable": False, + }} + discovery_fan.load_discovery_data(config) + assert len(discovery_fan.disc_templates) == 0 + + # test for overriding component + discovery_fan.device.config_extra['discovery_overrides'] = { 'fake': { + "component": "switch", + }} + discovery_fan.load_discovery_data(config) + expected_topic = "homeassistant/switch/11_22_33/unique/config" + assert discovery_fan.disc_templates[0].topic_str == expected_topic + discovery_fan.disc_templates = [] + + # test for overriding config unique_id + discovery_fan.device.config_extra['discovery_overrides'] = { 'fake': { + "config": { + "unique_id": "override", + }, + }} + discovery_fan.load_discovery_data(config) + expected_topic = "homeassistant/fan/11_22_33/override/config" + assert discovery_fan.disc_templates[0].topic_str == expected_topic + discovery_fan.disc_templates = [] + + # test for adding config attribute + discovery_fan.device.config_extra['discovery_overrides'] = { 'fake': { + "config": { + "foo": "fake", + }, + }} + discovery_fan.load_discovery_data(config) + payload = json.loads(discovery_fan.disc_templates[0].payload_str) + assert payload.get("foo", None) == "fake" + discovery_fan.disc_templates = [] + + # test for deleting config attribute + discovery_fan.device.config_extra['discovery_overrides'] = { 'fake': { + "config": { + "icon": "", + }, + }} + discovery_fan.load_discovery_data(config) + payload = json.loads(discovery_fan.disc_templates[0].payload_str) + assert "icon" not in payload + discovery_fan.disc_templates = [] + + # test for overriding device info + discovery_fan.device_info_template = {"info": "fake"} + discovery_fan.device.config_extra['discovery_overrides'] = { 'device': { + "info": "override", + }} + discovery_fan.load_discovery_data(config) + assert discovery_fan.device_info_template.get("info", None) == "override" + discovery_fan.disc_templates = [] + + # test for adding device info + discovery_fan.device_info_template = {"info": "fake"} + discovery_fan.device.config_extra['discovery_overrides'] = { 'device': { + "sa": "fake", + }} + discovery_fan.load_discovery_data(config) + assert discovery_fan.device_info_template.get("info", None) == "fake" + assert discovery_fan.device_info_template.get("sa", None) == "fake" + discovery_fan.disc_templates = [] + + # test for deletng device info + discovery_fan.device_info_template = {"info": "fake"} + discovery_fan.device.config_extra['discovery_overrides'] = { 'device': { + "mdl": "", + }} + discovery_fan.load_discovery_data(config) + assert "mdl" not in discovery_fan.device_info_template + discovery_fan.disc_templates = [] + + # test for suppressing one of multiple entities + config['fake_dev'] = {'discovery_entities': { + 'fake': { + "component": "fan", + "config": { + "unique_id": "unique", + }, + }, + 'extra': { + "component": "light", + "config": { + "unique_id": "unique", + }, + }, + }} + discovery_fan.device.config_extra['discovery_overrides'] = { 'fake': { + "discoverable": False, + }} + discovery_fan.load_discovery_data(config) + assert len(discovery_fan.disc_templates) == 1 + + + #----------------------------------------------------------------------- + def test_load_class_discovery_overrides(self, discovery_fan): + discovery_fan.mqtt.discovery_enabled = True + # build fake class to be used for tests + config = {} + config['fake_dev'] = {'discovery_entities': { + 'fake': { + "component": "fan", + "config": {"unique_id": "unique"}, + }, + }} + discovery_fan.device.config_extra['discovery_class'] = ['fake_dev'] + # Override data from this point + discovery_fan.discovery_template_data = mock.Mock(return_value={}) + + # test base + discovery_fan.load_discovery_data(config) + expected_topic = "homeassistant/fan/11_22_33/unique/config" + assert discovery_fan.disc_templates[0].topic_str == expected_topic + discovery_fan.disc_templates = [] + + # test with one discovery_class containing overrides + config['fake_override'] = {'discovery_overrides': { + 'fake': { + "config": { + "unique_id": "override", + }, + }, + }} + discovery_fan.device.config_extra['discovery_override_class'] = 'fake_override' + discovery_fan.load_discovery_data(config) + expected_topic = "homeassistant/fan/11_22_33/override/config" + assert discovery_fan.disc_templates[0].topic_str == expected_topic + discovery_fan.disc_templates = [] + + # test with multiple discovery_class containing overrides + config['fake_override1'] = {'discovery_overrides': { + 'fake': { + "config": { + "unique_id": "override", + }, + }, + }} + config['fake_override2'] = {'discovery_overrides': { + 'fake': { + "component": "light", + "config": { + "unique_id": "override2", + }, + }, + }} + discovery_fan.device.config_extra['discovery_override_class'] = ['fake_override1', 'fake_override2'] + discovery_fan.load_discovery_data(config) + expected_topic = "homeassistant/light/11_22_33/override2/config" + assert discovery_fan.disc_templates[0].topic_str == expected_topic + discovery_fan.disc_templates = [] + + # test with multiple discovery_class containing suppressions + config['fake_override1'] = {'discovery_overrides': { + 'fake': { + "discoverable": False, + }, + }} + config['fake_override2'] = {'discovery_overrides': { + 'fake': { + "discoverable": False, + }, + }} + discovery_fan.device.config_extra['discovery_override_class'] = ['fake_override1', 'fake_override2'] + discovery_fan.load_discovery_data(config) + assert len(discovery_fan.disc_templates) == 0 + + #----------------------------------------------------------------------- + def test_template_data(self, discovery_switch, caplog): # Test default values - data = discovery.discovery_template_data() + data = discovery_switch.discovery_template_data() assert data['address'] == "11.22.33" assert data['name'] == "11.22.33" assert data['name_user_case'] == "11.22.33" @@ -119,15 +384,15 @@ def test_template_data(self, discovery, caplog): assert data['sub_cat'] == 0 assert data['firmware'] == 0 assert data['modem_addr'] == "20.30.40" - assert data['device_info_template'] == "" + assert data['device_info'] == "{}" # Test with actual values - discovery.device.name = "test device" - discovery.device.name_user_case = "Test Device" - discovery.device.db.engine = 2 - discovery.device.db.desc = IM.catalog.find(0x02, 0x2a) - discovery.device.db.firmware = 0x45 - data = discovery.discovery_template_data() + discovery_switch.device.name = "test device" + discovery_switch.device.name_user_case = "Test Device" + discovery_switch.device.db.engine = 2 + discovery_switch.device.db.desc = IM.catalog.find(0x02, 0x2a) + discovery_switch.device.db.firmware = 0x45 + data = discovery_switch.discovery_template_data() assert data['name'] == "test device" assert data['name_user_case'] == "Test Device" assert data['engine'] == "i2cs" @@ -138,66 +403,116 @@ def test_template_data(self, discovery, caplog): assert data['sub_cat'] == 0x2a assert data['firmware'] == 0x45 assert data['modem_addr'] == "20.30.40" - assert data['device_info_template'] == "" + assert data['device_info'] == "{}" # test device info template - discovery.mqtt.device_info_template = """ - { - "ids": "{{address}}", - "mf": "Insteon", - "mdl": "{%- if model_number != 'Unknown' -%} - {{model_number}} - {{model_description}} - {%- elif dev_cat_name != 'Unknown' -%} - {{dev_cat_name}} - 0x{{'%0x' % sub_cat|int }} - {%- elif dev_cat == 0 and sub_cat == 0 -%} - No Info - {%- else -%} - 0x{{'%0x' % dev_cat|int }} - 0x{{'%0x' % sub_cat|int }} - {%- endif -%}", - "sw": "0x{{'%0x' % firmware|int }} - {{engine}}", - "name": "{{name_user_case}}", - "via_device": "{{modem_addr}}" - } - """ - data = discovery.discovery_template_data() - assert data['device_info_template'] == """ - { - "ids": "11.22.33", - "mf": "Insteon", - "mdl": "2477S - SwitchLinc Relay (Dual-Band)", - "sw": "0x45 - i2cs", - "name": "Test Device", - "via_device": "20.30.40" - } - """ + discovery_switch.device_info_template = { + "ids": "{{address}}", + "mf": "Insteon", + "mdl": "{%- if model_number != 'Unknown' -%}" + "{{model_number}} - {{model_description}}" + "{%- elif dev_cat_name != 'Unknown' -%}" + "{{dev_cat_name}} - 0x{{'%0x' % sub_cat|int }}" + "{%- elif dev_cat == 0 and sub_cat == 0 -%}" + "No Info" + "{%- else -%}" + "0x{{'%0x' % dev_cat|int }} - 0x{{'%0x' % sub_cat|int }}" + "{%- endif -%}", + "sw": "0x{{'%0x' % firmware|int }} - {{engine}}", + "name": "{{name_user_case}}", + "via_device": "{{modem_addr}}", + } + + data = discovery_switch.discovery_template_data() + assert data['device_info'] == \ +"""{ + "ids": "11.22.33", + "mf": "Insteon", + "mdl": "2477S - SwitchLinc Relay (Dual-Band)", + "sw": "0x45 - i2cs", + "name": "Test Device", + "via_device": "20.30.40" +}""" # test bad device info template - discovery.mqtt.device_info_template = " {% if bad = 1 %}" - data = discovery.discovery_template_data() + discovery_switch.device_info_template = { + "bad": "{% if bad = 1 %}" + } + data = discovery_switch.discovery_template_data() assert 'Error rendering device_info_template' in caplog.text caplog.clear() + #----------------------------------------------------------------------- + def test_device_info(self, discovery_fan): + discovery_fan.mqtt.discovery_enabled = True + # build and request fake class to be used for tests + config = {} + discovery_fan.device_info_template = { + "fake": "fake_device" + } + discovery_fan.device.config_extra['discovery_class'] = 'fake_dev' + + # test with 'device_info' in entity config + config['fake_dev'] = {'discovery_entities': { + 'fake': { + "component": "fan", + "config": { + "unique_id": "unique", + "device": "{{device_info}}", + }, + }, + }} + discovery_fan.load_discovery_data(config) + payload = discovery_fan.disc_templates[0].render_payload(discovery_fan.discovery_template_data(), silent=True) + assert payload == \ +"""{ + "unique_id": "unique", + "device": { + "fake": "fake_device" +} +}""" + discovery_fan.disc_templates = [] + + # test with 'device_info_template' in string-stlye entity config + config['fake_dev'] = {'discovery_entities': { + 'fake': { + "component": "fan", + "config": '{"unique_id": "unique", "device": {{device_info_template}}}', + }, + }} + discovery_fan.load_discovery_data(config) + payload = discovery_fan.disc_templates[0].render_payload(discovery_fan.discovery_template_data(), silent=True) + assert payload == \ +"""{"unique_id": "unique", "device": { + "fake": "fake_device" +}}""" + discovery_fan.disc_templates = [] + #----------------------------------------------------------------------- @mock.patch('time.time', mock.MagicMock(return_value=12345)) - def test_publish(self, discovery, caplog): - discovery.disc_templates.append(mock.Mock()) - discovery.publish_discovery() - data = {'address': '11.22.33', - 'availability_topic': '', - 'name': '11.22.33', - 'name_user_case': '11.22.33', - 'engine': 'Unknown', - 'model_number': 'Unknown', - 'model_description': 'Unknown', - 'dev_cat': 0, - 'dev_cat_name': 'Unknown', - 'sub_cat': 0, - 'firmware': 0, - 'modem_addr': '20.30.40', - 'device_info_template': '', - 'timestamp': 12345} - discovery.disc_templates[0].publish.assert_called_once_with( - discovery.mqtt, + def test_publish(self, discovery_switch): + discovery_switch.disc_templates.append(mock.Mock()) + discovery_switch.publish_discovery() + data = { + 'address': '11.22.33', + 'availability_topic': '', + 'name': '11.22.33', + 'name_user_case': '11.22.33', + 'engine': 'Unknown', + 'model_number': 'Unknown', + 'model_description': 'Unknown', + 'dev_cat': 0, + 'dev_cat_name': 'Unknown', + 'sub_cat': 0, + 'firmware': 0, + 'modem_addr': '20.30.40', + 'device_info': '{}', + # alias to support old configurations + 'device_info_template': '{}', + 'timestamp': 12345, + } + discovery_switch.disc_templates[0].publish.assert_called_once_with( + discovery_switch.mqtt, data, retain=False ) diff --git a/tests/test_config.py b/tests/test_config.py index 0f1da298..2f79cd7c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -101,6 +101,34 @@ def test_dns(self): val = IM.config.validate(file) assert val == "" + #----------------------------------------------------------------------- + def test_scenes_empty(self): + file = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'configs', 'scenes_empty.yaml') + val = IM.config.validate(file) + assert val == "" + + #----------------------------------------------------------------------- + def test_discovery_schema_good(self): + file = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'configs', 'discovery_schema_good.yaml') + val = IM.config.validate(file) + assert val == "" + + #----------------------------------------------------------------------- + def test_discovery_schema_bad(self): + configs = [ + ("discovery_schema_class_mixed.yaml", "'discovery_overrides' must not be present with 'discovery_entities'"), + ("discovery_schema_device_discoverable_overrides.yaml", "'component', 'config' must not be present with 'discoverable'"), + ("discovery_schema_device_unknown.yaml", "entry does not match a valid device entry format"), + ] + + for config in configs: + file = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'configs', config[0]) + val = IM.config.validate(file) + assert config[1] in val + #----------------------------------------------------------------------- def test_validate_addr(self): validator = IM.config.IMValidator()