From bdd066563383abca8ae844750771c94800fbcfb3 Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Thu, 4 May 2023 19:33:03 -0400 Subject: [PATCH 1/9] Initial commit --- custom_components/lennoxs30/binary_sensor.py | 8 +- .../lennoxs30/ble_device_21p02.py | 60 +++ .../lennoxs30/ble_device_22v25.py | 2 +- custom_components/lennoxs30/sensor.py | 10 +- .../system_04_furn_ac_zoning_ble.json | 477 ++++++++++++++++-- tests/test_binary_sensor_setup.py | 10 +- tests/test_devices.py | 2 +- tests/test_sensor_setup.py | 5 +- 8 files changed, 535 insertions(+), 39 deletions(-) create mode 100644 custom_components/lennoxs30/ble_device_21p02.py diff --git a/custom_components/lennoxs30/binary_sensor.py b/custom_components/lennoxs30/binary_sensor.py index a0f46ab..aeb61ae 100644 --- a/custom_components/lennoxs30/binary_sensor.py +++ b/custom_components/lennoxs30/binary_sensor.py @@ -25,6 +25,7 @@ from .base_entity import S30BaseEntityMixin from .binary_sensor_ble import BleCommStatusBinarySensor from .ble_device_22v25 import lennox_22v25_binary_sensors +from .ble_device_21p02 import lennox_21p02_binary_sensors from .const import ( MANAGER, UNIQUE_ID_SUFFIX_AUX_HI_AMBIENT_LOCKOUT, @@ -68,8 +69,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e continue sensor_list.append(BleCommStatusBinarySensor(hass, manager, system, ble_device)) + ble_sensors: dict = None if ble_device.controlModelNumber == "22V25": - for sensor_dict in lennox_22v25_binary_sensors: + ble_sensors = lennox_22v25_binary_sensors + elif ble_device.controlModelNumber == "21P02": + ble_sensors = lennox_21p02_binary_sensors + if ble_sensors: + for sensor_dict in ble_sensors: if sensor_dict["input_id"] not in ble_device.inputs: _LOGGER.error( "Error BleBinarySensor name [%s] sensor_name [%s] no input_id [%d]", diff --git a/custom_components/lennoxs30/ble_device_21p02.py b/custom_components/lennoxs30/ble_device_21p02.py new file mode 100644 index 0000000..f6b1e8b --- /dev/null +++ b/custom_components/lennoxs30/ble_device_21p02.py @@ -0,0 +1,60 @@ +"""Lennox BLE Air Quality Sensor""" +from homeassistant.helpers.entity import EntityCategory +from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass + +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, CONCENTRATION_PARTS_PER_MILLION + +lennox_21p02_sensors = [ + { + "input_id": 4000, + "name": "rssi", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.SIGNAL_STRENGTH, + "entity_category": EntityCategory.DIAGNOSTIC, + "uom": SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + }, + { + "input_id": 4003, + "name": "total powered time", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.DURATION, + "entity_category": EntityCategory.DIAGNOSTIC, + }, + { + "input_id": 4004, + "name": "ble rssi", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.SIGNAL_STRENGTH, + "entity_category": EntityCategory.DIAGNOSTIC, + "uom": SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + }, + { + "input_id": 4100, + "status_id": 4102, + "name": "pm2_5", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.PM25, + "uom": CONCENTRATION_PARTS_PER_MILLION, + }, + { + "input_id": 4103, + "status_id": 4104, + "name": "co2", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.CO2, + }, + { + "input_id": 4105, + "status_id": 4106, + "name": "voc", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + "uom": CONCENTRATION_PARTS_PER_MILLION, + }, +] + +lennox_21p02_binary_sensors = [ + {"input_id": 4001, "name": "alarm_status", "entity_category": EntityCategory.DIAGNOSTIC}, + {"input_id": 4002, "name": "device_state", "entity_category": EntityCategory.DIAGNOSTIC}, + {"input_id": 4107, "name": "idle_switch", "entity_category": EntityCategory.DIAGNOSTIC}, +] diff --git a/custom_components/lennoxs30/ble_device_22v25.py b/custom_components/lennoxs30/ble_device_22v25.py index 3d093ff..06f1de9 100644 --- a/custom_components/lennoxs30/ble_device_22v25.py +++ b/custom_components/lennoxs30/ble_device_22v25.py @@ -1,4 +1,4 @@ -"""Support for Lennoxs30 outdoor temperature sensor""" +"""Support for Lennox BLE Remote Sensor""" # pylint: disable=line-too-long from homeassistant.helpers.entity import EntityCategory from homeassistant.components.binary_sensor import BinarySensorDeviceClass diff --git a/custom_components/lennoxs30/sensor.py b/custom_components/lennoxs30/sensor.py index e18721d..bd1f0c3 100644 --- a/custom_components/lennoxs30/sensor.py +++ b/custom_components/lennoxs30/sensor.py @@ -44,6 +44,7 @@ ) from .helpers import helper_create_system_unique_id, helper_get_equipment_device_info, lennox_uom_to_ha_uom from .ble_device_22v25 import lennox_22v25_sensors +from .ble_device_21p02 import lennox_21p02_sensors from .sensor_ble import S40BleSensor from . import Manager @@ -119,8 +120,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e for ble_device in system.ble_devices.values(): if ble_device.deviceType == "tstat": continue - elif ble_device.controlModelNumber == "22V25": - for sensor_dict in lennox_22v25_sensors: + ble_sensors: dict = None + if ble_device.controlModelNumber == "22V25": + ble_sensors = lennox_22v25_sensors + elif ble_device.controlModelNumber == "21P02": + ble_sensors = lennox_21p02_sensors + if ble_sensors: + for sensor_dict in ble_sensors: if sensor_dict["input_id"] not in ble_device.inputs: _LOGGER.error( "Error S40BleSensor name [%s] sensor_name [%s] no input_id [%d]", diff --git a/tests/messages/system_04_furn_ac_zoning_ble.json b/tests/messages/system_04_furn_ac_zoning_ble.json index d25db46..61cda66 100644 --- a/tests/messages/system_04_furn_ac_zoning_ble.json +++ b/tests/messages/system_04_furn_ac_zoning_ble.json @@ -1674,8 +1674,8 @@ }, { "device": { - "deviceName": "", - "cfStatus": "unKnown", + "deviceName": "air_sensor", + "cfStatus": "configured", "uiRasSettings": { "senBasedParticpt": "off", "enableState": "enabled", @@ -1687,37 +1687,158 @@ "scheduleTill": "" } }, - "wdn": 0, + "wdn": 576, "zId": 0, - "deviceType": "unKnown", + "deviceType": "iaq", "config": { - "powerType": "unKnown", + "powerType": "linePowered", "features": [ { "id": 0, "feature": { - "name": "", - "szValues": 0, + "name": "Control Model Number", + "szValues": 1, "values": [ { "id": 0, - "value": "" + "value": "21P02" } ], - "fid": 0, - "unit": "" + "fid": 3000, + "unit": "", + "format": "nts" + } + }, + { + "id": 1, + "feature": { + "name": "Control Serial Number", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "22L325" + } + ], + "fid": 3001 + } + }, + { + "id": 2, + "feature": { + "name": "Control Hardware Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "A" + } + ], + "fid": 3002 + } + }, + { + "id": 3, + "feature": { + "name": "Control Software Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "04.00.0334" + } + ], + "fid": 3003 + } + }, + { + "id": 4, + "feature": { + "name": "Node Information", + "format": "uint8", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "5" + } + ], + "fid": 3004 + } + }, + { + "id": 5, + "feature": { + "name": "Control Bootloader Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "01.00.0016" + } + ], + "fid": 3005 + } + }, + { + "id": 6, + "feature": { + "name": "Communication Controller Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "04.00.0052" + } + ], + "fid": 3006 + } + }, + { + "id": 7, + "feature": { + "name": "Communication Controller Bootloader Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "01.10.0003" + } + ], + "fid": 3007 + } + }, + { + "id": 8, + "feature": { + "name": "Duct Mount Status", + "format": "uint8", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "0" + } + ], + "fid": 3100 } } ], "parameters": [ { "parameter": { - "name": "", - "pid": 0, - "defaultValue": "", - "enabled": false, - "szValues": 0, - "value": "", + "name": "Equipment Name", + "pid": 2000, + "defaultValue": "\u0015", + "enabled": true, + "szValues": 18, + "value": "Indoor Air Quality", "range": { "max": "", "min": "", @@ -1732,50 +1853,347 @@ } ] }, - "unit": "", + "unit": "Min value", "string": { - "max": "" - } + "max": "100" + }, + "format": "nts", + "descriptor": "string" }, "id": 0 + }, + { + "parameter": { + "name": "TX Power", + "format": "int8", + "pid": 2001, + "defaultValue": "19", + "enabled": true, + "szValues": 1, + "value": "19", + "descriptor": "range", + "range": { + "max": "20", + "min": "10", + "inc": "1" + }, + "unit": "Min value" + }, + "id": 1 + }, + { + "parameter": { + "name": "Enable or Disable Diagnostic Data In Device Status Message", + "format": "uint8", + "pid": 2003, + "defaultValue": "0", + "enabled": true, + "szValues": 1, + "value": "0", + "descriptor": "radio", + "radio": { + "max": "1", + "texts": [ + { + "text": "", + "id": 0 + }, + { + "text": "", + "id": 1 + } + ] + }, + "unit": "Min value" + }, + "id": 2 + }, + { + "parameter": { + "name": "Enable or Disable Logging", + "format": "uint8", + "pid": 2004, + "defaultValue": "0", + "enabled": true, + "szValues": 1, + "value": "0", + "descriptor": "radio", + "radio": { + "max": "1", + "texts": [ + { + "text": "", + "id": 0 + }, + { + "text": "", + "id": 1 + } + ] + }, + "unit": "Min value" + }, + "id": 3 + }, + { + "parameter": { + "name": "Low RSSI Threshold Value", + "format": "int8", + "pid": 2005, + "defaultValue": "170", + "enabled": true, + "szValues": 1, + "value": "-86", + "descriptor": "range", + "range": { + "max": "176", + "min": "156", + "inc": "1" + }, + "unit": "DBM" + }, + "id": 4 + }, + { + "parameter": { + "name": "Low RSSI Detection Counter", + "format": "uint8", + "pid": 2006, + "defaultValue": "5", + "enabled": true, + "szValues": 1, + "value": "1", + "descriptor": "range", + "range": { + "max": "10", + "min": "5", + "inc": "1" + }, + "unit": "Min value" + }, + "id": 5 } ], - "szFeatures": 0, - "szParameters": 0, + "szFeatures": 9, + "szParameters": 6, "writeAccess": "internal", "notification": { - "installerNote": "unknown", + "installerNote": "freshInstallation", "writeAccess": "internal", "doNotPersist": true } }, "writeAccess": "internal", "devStatus": { - "szStatus": 0, + "szStatus": 12, "doNotPersist": true, "inputsStatus": [ { "status": { - "name": "", - "vid": 0, + "name": "rssi", + "vid": 4000, "szValues": 0, "writeAccess": "internal", "values": [ { "id": 0, - "value": "" + "value": "-40" } ], "doNotPersist": true, - "unit": "" + "unit": "none", + "format": "int8" }, "doNotPersist": true, "writeAccess": "internal", "id": 0 + }, + { + "id": 1, + "status": { + "name": "Alarm status", + "vid": 4001, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + } + }, + { + "id": 2, + "status": { + "name": "Device State", + "vid": 4002, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "1" + } + ], + "unit": "none" + } + }, + { + "id": 3, + "status": { + "name": "Total Powered Time in Secs", + "vid": 4003, + "format": "uint32", + "values": [ + { + "id": 0, + "value": "109" + } + ], + "unit": "Sec" + } + }, + { + "id": 4, + "status": { + "name": "S40_BLE_RSSI", + "vid": 4004, + "format": "int8", + "values": [ + { + "id": 0, + "value": "-38" + } + ], + "unit": "none" + } + }, + { + "id": 5 + }, + { + "id": 6 + }, + { + "id": 7 + }, + { + "id": 8 + }, + { + "id": 9 + }, + { + "status": { + "name": "IAQ PM2_5", + "vid": 4100, + "format": "uint16", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 10 + }, + { + "id": 11 + }, + { + "status": { + "name": "IAQ PM2_5 Status", + "vid": 4102, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 12 + }, + { + "status": { + "name": "IAQ CO2", + "vid": 4103, + "format": "uint16", + "values": [ + { + "id": 0, + "value": "668" + } + ], + "unit": "none" + }, + "id": 13 + }, + { + "status": { + "name": "IAQ CO2 Status", + "vid": 4104, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 14 + }, + { + "status": { + "name": "IAQ VOC", + "vid": 4105, + "format": "uint16", + "values": [ + { + "id": 0, + "value": "1250" + } + ], + "unit": "none" + }, + "id": 15 + }, + { + "status": { + "name": "IAQ VOC Status", + "vid": 4106, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 16 + }, + { + "status": { + "name": "IDLE Switch Status", + "vid": 4107, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 17 } ], "writeAccess": "internal", - "commStatus": "unKnown" + "commStatus": "active" }, "userEdited": { "writeAccess": "openAll", @@ -1799,7 +2217,8 @@ "isParticipating": false, "writeAccess": "internal", "doNotPersist": true - } + }, + "deviceProvAddress": 8196 }, "writeAccess": "internal", "id": 3 diff --git a/tests/test_binary_sensor_setup.py b/tests/test_binary_sensor_setup.py index f4da236..1a245ce 100644 --- a/tests/test_binary_sensor_setup.py +++ b/tests/test_binary_sensor_setup.py @@ -74,7 +74,7 @@ async def test_async_binary_sensor_setup_entry(hass, manager: Manager, caplog): await async_setup_entry(hass, entry, async_add_entities) assert async_add_entities.called == 1 sensor_list = async_add_entities.call_args[0][0] - assert len(sensor_list) == 10 + assert len(sensor_list) == 14 assert isinstance(sensor_list[0], S30HomeStateBinarySensor) assert isinstance(sensor_list[1], S30CloudConnectedStatus) assert isinstance(sensor_list[2], BleCommStatusBinarySensor) @@ -85,6 +85,10 @@ async def test_async_binary_sensor_setup_entry(hass, manager: Manager, caplog): assert isinstance(sensor_list[7], BleBinarySensor) assert isinstance(sensor_list[8], BleBinarySensor) assert isinstance(sensor_list[9], BleBinarySensor) + assert isinstance(sensor_list[10], BleCommStatusBinarySensor) + assert isinstance(sensor_list[11], BleBinarySensor) + assert isinstance(sensor_list[12], BleBinarySensor) + assert isinstance(sensor_list[13], BleBinarySensor) with caplog.at_level(logging.ERROR): caplog.clear() @@ -94,7 +98,7 @@ async def test_async_binary_sensor_setup_entry(hass, manager: Manager, caplog): await async_setup_entry(hass, entry, async_add_entities) assert async_add_entities.called == 1 sensor_list = async_add_entities.call_args[0][0] - assert len(sensor_list) == 8 + assert len(sensor_list) == 12 assert len(caplog.records) == 2 assert system.ble_devices[512].deviceName in caplog.messages[0] @@ -115,7 +119,7 @@ async def test_async_binary_sensor_setup_entry(hass, manager: Manager, caplog): await async_setup_entry(hass, entry, async_add_entities) assert async_add_entities.called == 1 sensor_list = async_add_entities.call_args[0][0] - assert len(sensor_list) == 3 + assert len(sensor_list) == 7 assert len(caplog.records) == 1 assert system.ble_devices[513].deviceName in caplog.messages[0] diff --git a/tests/test_devices.py b/tests/test_devices.py index 7cb45b3..8eeec44 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -259,7 +259,7 @@ async def test_create_devices_furn_ac_zoning(hass, manager_system_04_furn_ac_zon system = manager.api.system_list[0] with patch.object(device_registry, "async_get_or_create") as mock_create_device: await manager.create_devices() - assert mock_create_device.call_count == 10 + assert mock_create_device.call_count == 11 assert len(manager.system_equip_device_map[system.sysId]) == 5 call = mock_create_device.mock_calls[0] diff --git a/tests/test_sensor_setup.py b/tests/test_sensor_setup.py index ec1e90a..6bd5a2b 100644 --- a/tests/test_sensor_setup.py +++ b/tests/test_sensor_setup.py @@ -248,7 +248,7 @@ async def test_async_setup_entry(hass, manager: Manager, caplog): await async_setup_entry(hass, entry, async_add_entities) assert async_add_entities.called == 1 sensor_list = async_add_entities.call_args[0][0] - assert len(sensor_list) == 16 + assert len(sensor_list) == 22 for index in range(0, 16): assert isinstance(sensor_list[index], S40BleSensor) @@ -260,7 +260,7 @@ async def test_async_setup_entry(hass, manager: Manager, caplog): await async_setup_entry(hass, entry, async_add_entities) assert async_add_entities.called == 1 sensor_list = async_add_entities.call_args[0][0] - assert len(sensor_list) == 14 + assert len(sensor_list) == 20 assert len(caplog.records) == 2 assert system.ble_devices[512].deviceName in caplog.messages[0] @@ -277,6 +277,7 @@ async def test_async_setup_entry(hass, manager: Manager, caplog): caplog.clear() system.ble_devices[513].controlModelNumber = "SOME_NEW_DEVICE" system.ble_devices.pop(512) + system.ble_devices.pop(576) async_add_entities = Mock() await async_setup_entry(hass, entry, async_add_entities) assert async_add_entities.called == 0 From 0c2624656a609fef25d8836dd7562fe504a652aa Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Sat, 6 May 2023 11:01:32 -0400 Subject: [PATCH 2/9] #235 - Update unique ids for S40 systems --- .coverage | Bin 69632 -> 69632 bytes coverage.xml | 1506 +++++++++++--------- custom_components/lennoxs30/__init__.py | 182 ++- custom_components/lennoxs30/config_flow.py | 73 +- custom_components/lennoxs30/manifest.json | 2 +- tests/test_config_flow.py | 44 +- tests/test_manager.py | 130 +- 7 files changed, 1185 insertions(+), 752 deletions(-) diff --git a/.coverage b/.coverage index c80d3d75d38133d7de78746a7563e34ae369efee..42dcd85bbe4361f91937eb4e2612f03c8dc6f04b 100644 GIT binary patch delta 828 zcmW+zO>7fK7*2fMpTiyD4?_Qjo`?Pl#Y3k1&Yl4&{G($_))e*`jtPXq0NCjWi^kN)%WD|tr# zRUVVi$Oq+5(Y796Q{ZBxM@sfe z{s>B_WBZ(o7dxO)FqqHt!c574J+T4m2Wg{i-nuwz>eeLoC z`Bb;4YxGdM|H?)>8;u>YRC+FMJJUSrF8^npJvVRL#p$co$?|M%ju(ufuM6c|t`eiu z(^m@%`O1Lm@@U*X8b9&fo1tUzJrk={mQc5C^)>~**m`lpSNZxI#!b!CSQ0!#-2}76 zsE;tmWYl%SpLaFnNtcvwlhTV<#^|0cf_h=gpJr;O$Fo#g97iX%4_JKAdCl&)T8^Po z;yeMU3zMr{6`uw);ALZV(KOoG+34EDaK ziYWGO1K075=!F18o)w3#K+p@(nFi4mIbEP|xubS~JGQ}+u48E6B?pWF2A~TeE~X99 zGP-~ZpK<8Vka526wbyoNxXEa#W3qp2so6g&c^jpA)4Rt%q7fJ?I57Nn*oYO1T<96F zS1^C~{i764e&jc|Rz4E7Xu_H`SQeTCgINqk`y{ITQQFZ_v$MA?sGbDZ45MuS;>Kb& q*Z*fv|JLooZHY7sXG6`>VX0wxyCj|Osx`NJy`CqV$>ihleW`cY0Qe&S delta 803 zcmW-eTWAwY9L6_ky4$4LO&>%N6l_roYW2Ytw z3;NiI!jYbnRxNGLsV7c-l3HvY#6}w8C|-&!>8RVfCYznlS%=|2Gk?CB@8dU#q%V>5 zJ*-X;RaZi#&_AL3q4l8^#=P;xcw=-KF=L&v(5TW){h@wC@6?a$d-N^(8hwc#2>uMd z555cz1e3vAwWGD++MR*F1MPv`fhGQ&zt6wTzt+FFX0&FYrn@Gn&1tW-$6B{`Lffrv z)|P4kbx!@HzEIQZUGU*Crdx@v1+T*|IZStO9@>N-$xU;8c1{GHzB_BrKvd&xV?bXa)qLG0-VsU*1ME5(6R;4%*skA$mM! zf%RqtXkW@knwMyCp3FGxEsC;;=v0;#eWSGKzj@}$zvNh>402&xvSd{9lIFr-lah@e8 z@s^v$yQ3GozY4-g>?0?t6z<=volwPAZpn%^hrZ=)vEa zA*{Et`5f&l_hM!`C65`aoMs)UgdzSI6j+$-2eW4OBAvFyG+l4U`+L - + @@ -14,9 +14,9 @@ - + - + @@ -91,8 +91,6 @@ - - @@ -103,126 +101,120 @@ - + + + - - - + + + + + - + - - + - - - - - - + - - + - + + - + + - - + - - - - - - - - - - + + + + + + + + + + - + + - - - - + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + @@ -230,271 +222,335 @@ - - - - - - + + - + + - + + + + + + - - - - + - - - - + - + - - - + + + + + + - + - - - + - - - - + - + - - - + - - - - + + - - - - + + + - - - + + + + - + - - + + + + + - + + + + + + - + + + + - - + - + + - - - - - + + + + + + - - - + - - - - - - - - - - + + - + + - - - - - - - + - - - - - - - + - + + + - - - - - - - + + + + + - + - + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1526,167 +1582,167 @@ - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - + + + + - - - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + - + - - + - - + + + + + - - - - - + - + - + + - - - - - - - + + + + + + + + - - + + + - - - + + - - - - + + - + + + + + + + + + @@ -2486,7 +2542,7 @@ - + @@ -2575,337 +2631,337 @@ - - + + + - - - - + + + - - + - + - + + - - + + + + - + + - + - - - - - + - + - + + + - + - - - - - - - - + + + + + + + + + - + - + + - - + + - - + - + - - + - - + + + + - + - - - - + + - + + + + + + - - - - - - + + - - + + - + - + + - + - - - + + + - + - - - - + + - + + + + + + - - - - - - + + - - + + - + - - + - + + + - + - + - + - - - + - + - + + + + + - - - - + + - - + - - - + + - + - - + + - - + + + + + + - - - - - + - + + + + - + + - - - - + - - - - + + + - + - + + - - + + + + + - - - + + + + + - - - - - - - - + + - - + @@ -2916,7 +2972,15 @@ + + + + + + + + @@ -3399,7 +3463,7 @@ - + @@ -6347,11 +6411,10 @@ - - + - + @@ -6373,8 +6436,8 @@ - - + + @@ -6398,10 +6461,10 @@ - + - + @@ -6423,8 +6486,8 @@ - - + + @@ -6448,10 +6511,10 @@ - + - + @@ -6473,7 +6536,7 @@ - + @@ -6490,11 +6553,11 @@ - - + + - + @@ -6516,7 +6579,7 @@ - + @@ -6539,10 +6602,10 @@ - + - + @@ -6563,7 +6626,7 @@ - + @@ -6586,10 +6649,10 @@ - + - + @@ -6613,8 +6676,8 @@ - - + + @@ -6638,10 +6701,10 @@ - + - + @@ -6664,9 +6727,9 @@ - - - + + + @@ -6689,11 +6752,11 @@ - - + + - + @@ -6716,9 +6779,9 @@ - - - + + + @@ -6742,145 +6805,144 @@ + - - + + - - + - + - + - + - + - + - - - - + + + + - + - + - + - + - + + - - + - + - + - @@ -6888,9 +6950,9 @@ + - @@ -6898,13 +6960,13 @@ + + - - + - @@ -6920,16 +6982,16 @@ + - + - @@ -6949,9 +7011,9 @@ + - @@ -6971,13 +7033,13 @@ + + - - + - @@ -6998,16 +7060,16 @@ + - + - @@ -7030,9 +7092,9 @@ + - @@ -7052,9 +7114,9 @@ + - @@ -7072,13 +7134,13 @@ + + - - + - @@ -7086,20 +7148,20 @@ + + - - + + - - + + - - @@ -7114,16 +7176,15 @@ + + - - - - - + + + - @@ -7133,85 +7194,89 @@ + - + + - - + - + - + - + + - - + - + - + + - - + - + - + + - - + - + - + + - - + + + + @@ -7750,113 +7815,112 @@ - - - - - - - + + + + + + - - + + - + - + - + + - - + - + + - - + - + + - - + - + - + + + - - - + + - - + @@ -7896,19 +7960,19 @@ + - - + - + @@ -7917,49 +7981,54 @@ - + - + + - - + - + + - - + - + + + + + + @@ -8297,7 +8366,7 @@ - + @@ -8309,11 +8378,11 @@ - - - - - + + + + + @@ -8324,8 +8393,8 @@ - - + + @@ -8334,8 +8403,8 @@ - - + + @@ -8348,15 +8417,15 @@ - + + - + - + - + - @@ -8371,15 +8440,15 @@ - - - + + + + - + - - + @@ -8392,16 +8461,16 @@ + - + - @@ -8413,8 +8482,8 @@ + - @@ -8425,8 +8494,8 @@ - - + + @@ -8440,8 +8509,8 @@ - - + + @@ -8455,8 +8524,8 @@ - - + + @@ -8469,14 +8538,14 @@ + - - - + + @@ -8489,14 +8558,14 @@ + - - - + + @@ -8506,10 +8575,10 @@ - - - - + + + + @@ -8519,18 +8588,18 @@ - - - - + + + + - - + + - - + + @@ -8540,21 +8609,21 @@ + - + - + - + - + - - + @@ -8564,44 +8633,44 @@ + - + - + - + - + - - - + + + - + - + - + - - - + + - - + + @@ -8609,11 +8678,11 @@ + - - - + + @@ -8628,8 +8697,8 @@ + - @@ -8647,16 +8716,16 @@ + - + - + - + - @@ -8669,12 +8738,12 @@ + - - - + + @@ -8692,20 +8761,20 @@ + - + - + - + - + - @@ -8721,21 +8790,21 @@ + - + - + - + - + - + - @@ -8751,21 +8820,21 @@ + - + - + - + - + - + - @@ -8781,22 +8850,22 @@ + - + - + - + - + - + - @@ -8811,24 +8880,24 @@ + - + - + - + - + - + - + - @@ -8844,20 +8913,20 @@ + - + - + - + - @@ -8877,21 +8946,21 @@ + - + - + - + - + - @@ -8906,19 +8975,19 @@ + - + - + - - - + + @@ -8928,17 +8997,17 @@ + - + - + - + - @@ -8948,15 +9017,15 @@ + - + - + - @@ -8966,14 +9035,14 @@ + - + - + - @@ -8983,14 +9052,14 @@ + - + - + - @@ -9000,16 +9069,16 @@ + - + - + - + - @@ -9019,52 +9088,131 @@ + - + - - - + + - - + + - + + - + - + - - - - - - + + + + + - + + - + - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/custom_components/lennoxs30/__init__.py b/custom_components/lennoxs30/__init__.py index 5de84ee..f999c17 100644 --- a/custom_components/lennoxs30/__init__.py +++ b/custom_components/lennoxs30/__init__.py @@ -14,7 +14,7 @@ from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er, device_registry as dr from homeassistant.helpers.typing import ConfigType from homeassistant.const import ( CONF_HOST, @@ -202,6 +202,9 @@ def create_migration_task(hass, migration_data): ) +g_unique_id_update: dict = {} + + def _upgrade_config(config: dict, current_version: int) -> int: if current_version == 1: config[CONF_FAST_POLL_COUNT] = 10 @@ -214,6 +217,10 @@ def _upgrade_config(config: dict, current_version: int) -> int: if config[CONF_CLOUD_CONNECTION] is False: config[CONF_CREATE_PARAMETERS] = False current_version = 4 + # Version 4 to 5 is a unique id update, flag it here. + if current_version == 4: + g_unique_id_update[4] = True + current_version = 5 return current_version @@ -241,7 +248,11 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Setup a config entry""" - _LOGGER.debug("async_setup_entry UniqueID [%s] Data [%s]", entry.unique_id, dict_redact_fields(entry.data)) + _LOGGER.debug( + "async_setup_entry UniqueID [%s] Data [%s]", + entry.unique_id, + dict_redact_fields(entry.data), + ) # Determine if this is the first entry that gets S30.State. global _FIRST_ENTRY_TITLE @@ -359,7 +370,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await manager.async_shutdown(None) except S30Exception as err: - _LOGGER.error("async_unload_entry entry [%s] error [%s]", entry.unique_id, err.as_string()) + _LOGGER.error( + "async_unload_entry entry [%s] error [%s]", + entry.unique_id, + err.as_string(), + ) except Exception: _LOGGER.exception("async_unload_entry entry - unexpected exception [%s]", entry.unique_id) return True @@ -435,10 +450,16 @@ def __init__( self.is_metric: bool = None if self._hass.config.units is US_CUSTOMARY_SYSTEM: - _LOGGER.info("Manager::init setting units to english - HASS Units [%s]", self._hass.config.units._name) + _LOGGER.info( + "Manager::init setting units to english - HASS Units [%s]", + self._hass.config.units._name, + ) self.is_metric = False else: - _LOGGER.info("Manager::init setting units to metric - HASS Units [%s]", self._hass.config.units._name) + _LOGGER.info( + "Manager::init setting units to metric - HASS Units [%s]", + self._hass.config.units._name, + ) self.is_metric = True self.connected = False self.last_cloud_presence_poll: float = None @@ -517,6 +538,8 @@ async def s30_initialize(self): self.updateState(DS_CONNECTING) await self.connect_subscribe() await self.configuration_initialization() + if len(g_unique_id_update) != 0: + await self.unique_id_updates() # Launch the message pump loop self._retrieve_task = asyncio.create_task(self.messagePump_task()) # Since there is no change detection implemented to update device attributes like SW version - alwayas reinit @@ -530,6 +553,84 @@ async def s30_initialize(self): self._climate_entities_initialized = True self.updateState(DS_CONNECTED) + async def unique_id_updates(self): + """Update Unique Ids for affected S40 systems, where the prefix of 123_ was being used""" + _LOGGER.info("unique_id_updates - checking for affected systems") + if not self.api.isLANConnection or len(self.api.system_list) != 1: + return + system = self.api.system_list[0] + if system.productType != "S40": + return + + _LOGGER.info("Updating unique ids for connection [%s]", self.api.ip) + try: + await self._update_entity_unique_ids(system) + except Exception as e: + _LOGGER.exception( + "Failed to update entity unique_ids connection [%s] [%s]", + self.api.ip, + e, + ) + try: + await self._update_device_unique_ids(system) + except Exception as e: + _LOGGER.exception( + "Failed to update device unique_ids connection [%s] [%s]", + self.api.ip, + e, + ) + + async def _update_entity_unique_ids(self, system: lennox_system): + _LOGGER.info("Updating entity unique ids for connection [%s]", self.api.ip) + ent_reg = er.async_get(self._hass) + entity_update_list: dict[str, str] = {} + for regentry in ent_reg.entities.values(): + if regentry.config_entry_id == self.config_entry.entry_id and regentry.platform == LENNOX_DOMAIN: + if regentry.unique_id.startswith("123_"): + suffix = regentry.unique_id.removeprefix("123_") + new_unique_id = f"{system.unique_id}_{suffix}".replace("-", "") + entity_update_list[regentry.entity_id] = new_unique_id + _LOGGER.info( + "Updating entity [%s] unique id [%s] new unique id [%s]", + regentry.entity_id, + regentry.unique_id, + new_unique_id, + ) + for k, v in entity_update_list.items(): + _LOGGER.info("Committing new entity unique ids for connection [%s] [%s] [%s]", self.api.ip, k, v) + ent_reg.async_update_entity(k, new_unique_id=v) + + async def _update_device_unique_ids(self, system: lennox_system): + dev_reg = dr.async_get(self._hass) + device_update_list: dict[str, str] = {} + for regentry in dev_reg.devices.values(): + if self.config_entry.entry_id in regentry.config_entries: + for x in regentry.identifiers: + if x[0] == LENNOX_DOMAIN: + unique_id = x[1] + if unique_id.startswith("123_"): + suffix = unique_id.removeprefix("123_") + new_unique_id = f"{system.unique_id}_{suffix}".replace("-", "") + device_update_list[regentry.id] = new_unique_id + _LOGGER.info( + "Updating device [%s] identifier [%s] new unique id [%s]", + regentry.id, + unique_id, + new_unique_id, + ) + elif unique_id == "123": + new_unique_id = system.unique_id.replace("-", "") + device_update_list[regentry.id] = new_unique_id + _LOGGER.info( + "Updating device [%s] identifier [%s] new unique id [%s]", + regentry.id, + unique_id, + new_unique_id, + ) + for k, v in device_update_list.items(): + _LOGGER.info("Committing new device unique ids for connection [%s] [%s] [%s]", self.api.ip, k, v) + dev_reg.async_update_device(k, new_identifiers={(LENNOX_DOMAIN, v)}) + async def create_devices(self): """Creates devices for the discoved lennox equipment""" for system in self.api.system_list: @@ -589,14 +690,28 @@ async def initialize_retry_task(self): if e.error_code == EC_LOGIN: # TODO: encapsulate in manager class self.updateState(DS_LOGIN_FAILED) - _LOGGER.error("initialize_retry_task host [%s] %s", self._ip_address, e.as_string()) + _LOGGER.error( + "initialize_retry_task host [%s] %s", + self._ip_address, + e.as_string(), + ) return elif e.error_code == EC_CONFIG_TIMEOUT: _LOGGER.warning("async_setup: host [%s] %s", self._ip_address, e.as_string()) - _LOGGER.info("connection host [%s] will be retried in 1 minute", self._ip_address) + _LOGGER.info( + "connection host [%s] will be retried in 1 minute", + self._ip_address, + ) else: - _LOGGER.error("async_setup host [%s] unexpected error %s", self._ip_address, e.as_string()) - _LOGGER.info("async setup host [%s] will be retried in 1 minute", self._ip_address) + _LOGGER.error( + "async_setup host [%s] unexpected error %s", + self._ip_address, + e.as_string(), + ) + _LOGGER.info( + "async setup host [%s] will be retried in 1 minute", + self._ip_address, + ) async def configuration_initialization(self) -> None: """Waits for the configuration to arrive""" @@ -708,7 +823,11 @@ async def update_cloud_presence(self): await system.update_system_online_cloud() new_status = system.cloud_status if new_status == "offline" and old_status == "online": - _LOGGER.error("cloud status changed to offline for sysId [%s] name [%s]", system.sysId, system.name) + _LOGGER.error( + "cloud status changed to offline for sysId [%s] name [%s]", + system.sysId, + system.name, + ) elif old_status == "offline" and new_status == "online": _LOGGER.info( "cloud status changed to online for sysId [%s] name [%s] - resubscribing", @@ -719,7 +838,9 @@ async def update_cloud_presence(self): await self.api.subscribe(system) except S30Exception as e: _LOGGER.error( - "update_cloud_presence resubscribe error sysid [%s] error %s", system.sysId, e.as_string() + "update_cloud_presence resubscribe error sysid [%s] error %s", + system.sysId, + e.as_string(), ) self._reinitialize = True except Exception as e: @@ -731,9 +852,17 @@ async def update_cloud_presence(self): self._reinitialize = True except S30Exception as e: - _LOGGER.error("update_cloud_presence sysid [%s] error %s", system.sysId, e.as_string()) + _LOGGER.error( + "update_cloud_presence sysid [%s] error %s", + system.sysId, + e.as_string(), + ) except Exception as e: - _LOGGER.exception("update_cloud_presence unexpected exception sysid [%s] error %s", system.sysId, e) + _LOGGER.exception( + "update_cloud_presence unexpected exception sysid [%s] error %s", + system.sysId, + e, + ) def get_reinitialize(self): """Determine if object is reinitializing""" @@ -780,9 +909,15 @@ async def messagePump_task(self) -> None: elif self.get_reinitialize(): self.updateState(DS_DISCONNECTED) asyncio.create_task(self.reinitialize_task()) - _LOGGER.debug("messagePump_task host [%s] is exiting - to enter retries", self._ip_address) + _LOGGER.debug( + "messagePump_task host [%s] is exiting - to enter retries", + self._ip_address, + ) else: - _LOGGER.error("messagePump_task host [%s] is exiting - and this should not happen", self._ip_address) + _LOGGER.error( + "messagePump_task host [%s] is exiting - and this should not happen", + self._ip_address, + ) async def messagePump(self) -> bool: """Read and process a message""" @@ -797,16 +932,27 @@ async def messagePump(self) -> bool: self._err_cnt += 1 # This should mean we have been logged out and need to start the login process if e.error_code == EC_UNAUTHORIZED: - _LOGGER.warning("messagePump host [%s] - unauthorized - trying to relogin", self._ip_address) + _LOGGER.warning( + "messagePump host [%s] - unauthorized - trying to relogin", + self._ip_address, + ) self._reinitialize = True # If its an HTTP error, we will not log an error, just and info message, unless # this exceeds the max consecutive error count elif e.error_code == EC_HTTP_ERR and self._err_cnt < MAX_ERRORS: - _LOGGER.debug("messagePump http error host [%s] %s", self._ip_address, e.as_string()) + _LOGGER.debug( + "messagePump http error host [%s] %s", + self._ip_address, + e.as_string(), + ) # Since the S30 will close connections and kill the subscription periodically, these errors # are expected. Log as warnings elif e.error_code == EC_COMMS_ERROR: - _LOGGER.warning("messagePump communication error host [%s] %s", self._ip_address, e.as_string()) + _LOGGER.warning( + "messagePump communication error host [%s] %s", + self._ip_address, + e.as_string(), + ) else: _LOGGER.warning("messagePump error host [%s] %s", self._ip_address, e.as_string()) bErr = True diff --git a/custom_components/lennoxs30/config_flow.py b/custom_components/lennoxs30/config_flow.py index 0aa5fd0..6776408 100644 --- a/custom_components/lennoxs30/config_flow.py +++ b/custom_components/lennoxs30/config_flow.py @@ -1,8 +1,30 @@ +"""Integration Configuration""" +# pylint: disable=attribute-defined-outside-init +# pylint: disable=line-too-long +# pylint: disable=missing-function-docstring + import ipaddress +import logging import re +import voluptuous as vol + + +from homeassistant.data_entry_flow import FlowResult +from homeassistant import config_entries +from homeassistant.core import HomeAssistant, callback +from homeassistant.const import ( + CONF_HOST, + CONF_EMAIL, + CONF_PASSWORD, + CONF_PROTOCOL, + CONF_SCAN_INTERVAL, + CONF_TIMEOUT, +) +from homeassistant.helpers import config_validation as cv + + from lennoxs30api.s30exception import EC_LOGIN, S30Exception -import voluptuous as vol from . import Manager from .const import ( CONF_ALLERGEN_DEFENDER_SWITCH, @@ -26,19 +48,6 @@ CONF_CREATE_PARAMETERS, ) from .util import dict_redact_fields, redact_email -from homeassistant.data_entry_flow import FlowResult -from homeassistant import config_entries -from homeassistant.core import HomeAssistant, callback -from homeassistant.const import ( - CONF_HOST, - CONF_EMAIL, - CONF_PASSWORD, - CONF_PROTOCOL, - CONF_SCAN_INTERVAL, - CONF_TIMEOUT, -) -from homeassistant.helpers import config_validation as cv -import logging DEFAULT_POLL_INTERVAL: int = 10 @@ -100,10 +109,10 @@ def lennox30_entries(hass: HomeAssistant): return set(entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)) -class lennoxs30ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class Lennoxs30ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Lennox S30 configflow.""" - VERSION = 4 + VERSION = 5 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def _host_in_configuration_exists(self, host) -> bool: @@ -147,15 +156,15 @@ async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} self.config_input = {} - _LOGGER.debug(f"async_step_user user_input [{dict_redact_fields(user_input)}]") + _LOGGER.debug("async_step_user user_input [%s]", dict_redact_fields(user_input)) if user_input is not None: cloud_connection = user_input[CONF_CLOUD_CONNECTION] local_connection = user_input[CONF_LOCAL_CONNECTION] if cloud_connection == local_connection: errors[CONF_LOCAL_CONNECTION] = "select_cloud_or_local" else: - dict = {CONF_CLOUD_CONNECTION: cloud_connection} - self.config_input.update(dict) + update_dict = {CONF_CLOUD_CONNECTION: cloud_connection} + self.config_input.update(update_dict) if cloud_connection: return await self.async_step_cloud() else: @@ -166,7 +175,7 @@ async def async_step_user(self, user_input=None): async def async_step_cloud(self, user_input=None): """Handle the initial step.""" errors = {} - _LOGGER.debug(f"async_step_cloud user_input [{dict_redact_fields(user_input)}]") + _LOGGER.debug("async_step_cloud user_input [%s]", dict_redact_fields(user_input)) if user_input is not None: await self.async_set_unique_id(DOMAIN + "_" + user_input[CONF_EMAIL]) self._abort_if_unique_id_configured() @@ -174,9 +183,9 @@ async def async_step_cloud(self, user_input=None): await self.try_to_connect(user_input) self.config_input.update(user_input) return await self.async_step_advanced() - except S30Exception as e: - _LOGGER.error(f"async_step_cloud error [{e.as_string()}]") - if e.error_code == EC_LOGIN: + except S30Exception as ex: + _LOGGER.error("async_step_cloud error [%s]", ex.as_string()) + if ex.error_code == EC_LOGIN: errors["base"] = "unable_to_connect_login" else: errors["base"] = "unable_to_connect_cloud" @@ -185,7 +194,7 @@ async def async_step_cloud(self, user_input=None): async def async_step_local(self, user_input=None): """Handle the initial step.""" errors = {} - _LOGGER.debug(f"async_step_local user_input [{dict_redact_fields(user_input)}]") + _LOGGER.debug("async_step_local user_input [%s]", dict_redact_fields(user_input)) if user_input is not None: host = user_input[CONF_HOST] @@ -200,14 +209,14 @@ async def async_step_local(self, user_input=None): await self.try_to_connect(user_input) self.config_input.update(user_input) return await self.async_step_advanced() - except S30Exception as e: - _LOGGER.error(f"async_step_local error [{e.as_string()}]") + except S30Exception as ex: + _LOGGER.error("async_step_local error [%s]", ex.as_string()) errors[CONF_HOST] = "unable_to_connect_local" return self.async_show_form(step_id="local", data_schema=STEP_LOCAL, errors=errors) async def async_step_advanced(self, user_input=None): errors = {} - _LOGGER.debug(f"async_step_advanced user_input [{dict_redact_fields(user_input)}]") + _LOGGER.debug("async_step_advanced user_input [%s]", dict_redact_fields(user_input)) if user_input is not None: self.config_input.update(user_input) @@ -227,7 +236,7 @@ async def create_entry(self): self._abort_if_unique_id_configured() if self.config_input[CONF_LOG_MESSAGES_TO_FILE] is False: self.config_input[CONF_MESSAGE_DEBUG_FILE] = "" - _LOGGER.debug(f"async_step_advanced config_input [{dict_redact_fields(self.config_input)}]") + _LOGGER.debug("async_step_advanced config_input [%s]", dict_redact_fields(self.config_input)) return self.async_create_entry(title=title, data=self.config_input) async def try_to_connect(self, user_input): @@ -270,7 +279,7 @@ async def try_to_connect(self, user_input): async def async_step_import(self, user_input) -> FlowResult: """Handle the import step.""" self.config_input = {} - _LOGGER.debug(f"async_step_import user_input [{dict_redact_fields(user_input)}]") + _LOGGER.debug("async_step_import user_input [%s]", dict_redact_fields(user_input)) self.config_input.update(user_input) return await self.create_entry() @@ -281,6 +290,8 @@ def async_get_options_flow(config_entry): class OptionsFlowHandler(config_entries.OptionsFlow): + """Classs to handle options flow""" + def __init__(self, config_entry: config_entries.ConfigEntry): """Initialize options flow.""" self.config_entry = config_entry @@ -288,7 +299,9 @@ def __init__(self, config_entry: config_entries.ConfigEntry): async def async_step_init(self, user_input=None): """Manage the options.""" _LOGGER.debug( - f"OptionsFlowHandler:async_step_init user_input [{dict_redact_fields(user_input)}] data [{dict_redact_fields(self.config_entry.data)}]" + "OptionsFlowHandler:async_step_init user_input [%s] data [%s]", + dict_redact_fields(user_input), + dict_redact_fields(self.config_entry.data), ) if user_input is not None: if CONF_HOST in self.config_entry.data: diff --git a/custom_components/lennoxs30/manifest.json b/custom_components/lennoxs30/manifest.json index f5ca217..1b2f8a3 100644 --- a/custom_components/lennoxs30/manifest.json +++ b/custom_components/lennoxs30/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "issue_tracker" : "https://github.com/PeteRager/lennoxs30/issues", "quality_scale": "platinum", - "requirements": ["lennoxs30api==0.2.3"], + "requirements": ["lennoxs30api==0.2.4"], "version": "2023.5.0" } \ No newline at end of file diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 64b5acf..da8281d 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -37,7 +37,7 @@ from custom_components.lennoxs30.config_flow import ( OptionsFlowHandler, host_valid, - lennoxs30ConfigFlow, + Lennoxs30ConfigFlow, STEP_CLOUD, STEP_LOCAL, STEP_ONE, @@ -72,6 +72,7 @@ Manager, async_migrate_entry, async_setup, + g_unique_id_update, ) from custom_components.lennoxs30.util import redact_email @@ -118,7 +119,7 @@ async def test_migrate_local_config_min(hass, caplog): assert len(caplog.records) == 1 - config_flow = lennoxs30ConfigFlow() + config_flow = Lennoxs30ConfigFlow() with patch.object(config_flow, "async_set_unique_id") as _: result = await config_flow.async_step_import(migration_data) assert result["title"] == "10.0.0.1" @@ -188,7 +189,7 @@ async def test_migrate_local_config_full(hass, caplog): assert len(caplog.records) == 1 - config_flow = lennoxs30ConfigFlow() + config_flow = Lennoxs30ConfigFlow() with patch.object(config_flow, "async_set_unique_id") as _: result = await config_flow.async_step_import(migration_data) assert result["title"] == "10.0.0.1" @@ -315,7 +316,7 @@ async def test_migrate_cloud_config_min(hass, caplog): assert migration_data[CONF_FAST_POLL_COUNT] == 10 assert migration_data[CONF_TIMEOUT] == DEFAULT_CLOUD_TIMEOUT - config_flow = lennoxs30ConfigFlow() + config_flow = Lennoxs30ConfigFlow() with patch.object(config_flow, "async_set_unique_id") as _: result = await config_flow.async_step_import(migration_data) assert result["title"] == redact_email(migration_data[CONF_EMAIL]) @@ -381,7 +382,7 @@ async def test_migrate_cloud_config_full(hass, caplog): assert migration_data[CONF_FAST_POLL_COUNT] == 10 assert migration_data[CONF_TIMEOUT] == DEFAULT_CLOUD_TIMEOUT - config_flow = lennoxs30ConfigFlow() + config_flow = Lennoxs30ConfigFlow() with patch.object(config_flow, "async_set_unique_id") as _: result = await config_flow.async_step_import(migration_data) assert result["title"] == redact_email(migration_data[CONF_EMAIL]) @@ -430,7 +431,7 @@ async def test_upgrade_config_v1(hass): await async_migrate_entry(hass, config_entry) assert update_entry.call_count == 1 new_data = update_entry.call_args_list[0].kwargs["data"] - assert config_entry.version == 4 + assert config_entry.version == 5 assert new_data["cloud_connection"] is False assert new_data["host"] == "192.168.1.93" assert new_data["app_id"] == "homeassistant" @@ -473,7 +474,7 @@ async def test_upgrade_config_v1(hass): await async_migrate_entry(hass, config_entry) assert update_entry.call_count == 1 new_data = update_entry.call_args_list[0].kwargs["data"] - assert config_entry.version == 4 + assert config_entry.version == 5 assert new_data["cloud_connection"] is True assert new_data["email"] == "pete@pete.com" assert new_data["password"] == "secret" @@ -520,7 +521,7 @@ async def test_upgrade_config_v2(hass): await async_migrate_entry(hass, config_entry) assert update_entry.call_count == 1 new_data = update_entry.call_args_list[0].kwargs["data"] - assert config_entry.version == 4 + assert config_entry.version == 5 assert new_data["cloud_connection"] is False assert new_data["host"] == "192.168.1.93" assert new_data["app_id"] == "homeassistant" @@ -566,7 +567,7 @@ async def test_upgrade_config_v2(hass): await async_migrate_entry(hass, config_entry) assert update_entry.call_count == 1 new_data = update_entry.call_args_list[0].kwargs["data"] - assert config_entry.version == 4 + assert config_entry.version == 5 assert new_data["cloud_connection"] is True assert new_data["email"] == "pete@pete.com" assert new_data["password"] == "secret" @@ -615,7 +616,7 @@ async def test_upgrade_config_v3(hass, caplog): await async_migrate_entry(hass, config_entry) assert update_entry.call_count == 1 new_data = update_entry.call_args_list[0].kwargs["data"] - assert config_entry.version == 4 + assert config_entry.version == 5 assert new_data["cloud_connection"] is False assert new_data["host"] == "192.168.1.93" assert new_data["app_id"] == "homeassistant" @@ -661,7 +662,7 @@ async def test_upgrade_config_v3(hass, caplog): await async_migrate_entry(hass, config_entry) assert update_entry.call_count == 1 new_data = update_entry.call_args_list[0].kwargs["data"] - assert config_entry.version == 4 + assert config_entry.version == 5 assert new_data["cloud_connection"] is True assert new_data["email"] == "pete@pete.com" assert new_data["password"] == "secret" @@ -681,6 +682,8 @@ async def test_upgrade_config_v3(hass, caplog): assert new_data["timeout"] == DEFAULT_CLOUD_TIMEOUT assert new_data["create_diagnostic_sensors"] is False + assert len(g_unique_id_update) != 0 + def test_config_flow_host_valid(hass, caplog): assert host_valid("10.23.23.45") is True @@ -692,7 +695,7 @@ def test_config_flow_host_valid(hass, caplog): def test_lennoxS30ConfigFlow(manager: Manager, hass, caplog): - cf = lennoxs30ConfigFlow() + cf = Lennoxs30ConfigFlow() cf.hass = hass assert cf._host_in_configuration_exists("localhost") is False @@ -813,7 +816,7 @@ def test_lennoxS30ConfigFlow(manager: Manager, hass, caplog): @pytest.mark.asyncio async def test_lennoxS30ConfigFlow_async_step_user(manager: Manager, hass, caplog): - cf = lennoxs30ConfigFlow() + cf = Lennoxs30ConfigFlow() cf.hass = hass res = await cf.async_step_user(user_input=None) assert res["type"] == "form" @@ -866,7 +869,7 @@ async def test_lennoxS30ConfigFlow_async_step_user(manager: Manager, hass, caplo @pytest.mark.asyncio async def test_lennoxS30ConfigFlow_async_step_cloud(manager: Manager, hass, caplog): - cf = lennoxs30ConfigFlow() + cf = Lennoxs30ConfigFlow() cf.hass = hass with patch.object(cf, "async_set_unique_id") as async_set_unique_id: @@ -945,7 +948,7 @@ async def test_lennoxS30ConfigFlow_async_step_cloud(manager: Manager, hass, capl @pytest.mark.asyncio async def test_lennoxS30ConfigFlow_async_step_local(manager: Manager, hass, caplog): - cf = lennoxs30ConfigFlow() + cf = Lennoxs30ConfigFlow() cf.hass = hass with patch.object(cf, "async_set_unique_id") as async_set_unique_id: @@ -1053,7 +1056,7 @@ async def test_lennoxS30ConfigFlow_async_step_local(manager: Manager, hass, capl @pytest.mark.asyncio async def test_lennoxS30ConfigFlow_async_step_advanced(manager: Manager, hass, caplog): - cf = lennoxs30ConfigFlow() + cf = Lennoxs30ConfigFlow() cf.hass = hass with patch.object(cf, "create_entry") as create_entry: @@ -1070,7 +1073,7 @@ async def test_lennoxS30ConfigFlow_async_step_advanced(manager: Manager, hass, c @pytest.mark.asyncio async def test_lennoxS30ConfigFlow_async_get_options_flow(manager: Manager, hass, caplog): - cf = lennoxs30ConfigFlow().async_get_options_flow(manager.config_entry) + cf = Lennoxs30ConfigFlow().async_get_options_flow(manager.config_entry) cf.hass = hass assert isinstance(cf, OptionsFlowHandler) @@ -1083,7 +1086,7 @@ async def test_OptionsFlowHandler_async_step_init_local(config_entry_local, hass # TODO validate each scheme element schema = res["data_schema"].schema - + # pylint: disable=unused-variable si = schema[CONF_APP_ID] si = schema[CONF_CREATE_SENSORS] si = schema[CONF_ALLERGEN_DEFENDER_SWITCH] @@ -1111,6 +1114,7 @@ async def test_OptionsFlowHandler_async_step_init_cloud(config_entry_cloud, hass # TODO validate each scheme element schema = res["data_schema"].schema + # pylint: disable=unused-variable si = schema[CONF_PASSWORD] si = schema[CONF_APP_ID] si = schema[CONF_CREATE_SENSORS] @@ -1179,7 +1183,7 @@ async def test_OptionsFlowHandler_async_step_init_local_save( @pytest.mark.asyncio async def test_lennoxS30ConfigFlow_try_to_connect_cloud(manager: Manager, hass, caplog): - cf = lennoxs30ConfigFlow() + cf = Lennoxs30ConfigFlow() cf.config_input = {} cf.config_input[CONF_CLOUD_CONNECTION] = True cf.hass = hass @@ -1202,7 +1206,7 @@ async def test_lennoxS30ConfigFlow_try_to_connect_cloud(manager: Manager, hass, @pytest.mark.asyncio async def test_lennoxS30ConfigFlow_try_to_connect_local(manager: Manager, hass, caplog): - cf = lennoxs30ConfigFlow() + cf = Lennoxs30ConfigFlow() cf.config_input = {} cf.config_input[CONF_CLOUD_CONNECTION] = False cf.hass = hass diff --git a/tests/test_manager.py b/tests/test_manager.py index 2899188..cfe7d10 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM, METRIC_SYSTEM from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er, device_registry as dr from lennoxs30api.s30api_async import ( lennox_system, @@ -41,6 +42,7 @@ RETRY_INTERVAL_SECONDS, Manager, ) +from custom_components.lennoxs30.const import LENNOX_DOMAIN @pytest.mark.asyncio @@ -956,12 +958,12 @@ async def test_manager_async_shutdown_s30_initialize(manager_us_customary_units: @pytest.mark.asyncio -async def test_manager_async_shutdown_reinitialize(manager_us_customary_units: Manager, caplog): +async def test_manager_async_shutdown_reinitialize(manager_us_customary_units: Manager): manager = manager_us_customary_units manager._climate_entities_initialized = True - with patch.object(manager, "messagePump") as messagePump, patch.object( - manager, "connect_subscribe" - ) as connect_subscribe, patch.object(manager.api, "shutdown") as api_shutdown: + with patch.object(manager, "messagePump") as messagePump, patch.object(manager, "connect_subscribe"), patch.object( + manager.api, "shutdown" + ): messagePump.return_value = False await manager.reinitialize_task() @@ -979,6 +981,126 @@ async def test_manager_async_shutdown_reinitialize(manager_us_customary_units: M assert ex is None +@pytest.mark.asyncio +async def test_manager_unique_id_update(hass, manager_us_customary_units: Manager): + manager = manager_us_customary_units + system = manager.api.system_list[0] + system.productType = "S40" + entry_id = manager.config_entry.entry_id + + ent_reg = er.async_get(hass) + ent_reg.async_get_or_create( + "switch", LENNOX_DOMAIN, "123_HA", suggested_object_id="away", config_entry=manager.config_entry + ) + ent_reg.async_get_or_create( + "sensor", LENNOX_DOMAIN, "1234_HA", suggested_object_id="temperature", config_entry=manager.config_entry + ) + ent_reg.async_get_or_create("sensor", "other_domain", "123_HA", suggested_object_id="humidity") + ent_reg.async_get_or_create( + "climate", LENNOX_DOMAIN, "123_CL_ZONE1", suggested_object_id="zone1", config_entry=manager.config_entry + ) + + await manager.unique_id_updates() + + assert ent_reg.async_get("switch.away").unique_id == f"{system.unique_id}_HA".replace("-", "") + assert ent_reg.async_get("sensor.temperature").unique_id == "1234_HA" + assert ent_reg.async_get("sensor.humidity").unique_id == "123_HA" + assert ent_reg.async_get("climate.zone1").unique_id == f"{system.unique_id}_CL_ZONE1".replace("-", "") + + entry_id = manager.config_entry.entry_id + + dev_reg = dr.async_get(hass) + id1 = dev_reg.async_get_or_create(config_entry_id=entry_id, name="S30", identifiers={("lennoxs30", "123")}).id + id2 = dev_reg.async_get_or_create( + config_entry_id=entry_id, name="Indoor Unit", identifiers={("lennoxs30", "123_iu")} + ).id + id3 = dev_reg.async_get_or_create( + config_entry_id=entry_id, name="Outdoor Unit", identifiers={("lennoxs30", "1234_ou")} + ).id + id4 = dev_reg.async_get_or_create(config_entry_id="12345", name="Other", identifiers={("other", "123")}).id + + await manager.unique_id_updates() + + entry = dev_reg.async_get(id1) + unique_id = None + for unique_id in entry.identifiers: + break + assert unique_id[1] == system.unique_id.replace("-", "") + + entry = dev_reg.async_get(id2) + for unique_id in entry.identifiers: + break + assert unique_id[1] == f"{system.unique_id}_iu".replace("-", "") + + entry = dev_reg.async_get(id3) + for unique_id in entry.identifiers: + break + assert unique_id[1] == "1234_ou" + + entry = dev_reg.async_get(id4) + for unique_id in entry.identifiers: + break + assert unique_id[1] == "123" + + +@pytest.mark.asyncio +async def test_manager_unique_id_update_nop(manager_us_customary_units: Manager): + manager = manager_us_customary_units + + with patch.object(manager, "_update_device_unique_ids") as patch_update_device_unique_ids, patch.object( + manager, "_update_entity_unique_ids" + ) as patch__update_entity_unique_ids: + await manager.unique_id_updates() + assert patch_update_device_unique_ids.call_count == 0 + assert patch__update_entity_unique_ids.call_count == 0 + + system = manager.api.system_list[0] + system.productType = "S40" + manager.api.isLANConnection = False + with patch.object(manager, "_update_device_unique_ids") as patch_update_device_unique_ids, patch.object( + manager, "_update_entity_unique_ids" + ) as patch__update_entity_unique_ids: + await manager.unique_id_updates() + assert patch_update_device_unique_ids.call_count == 0 + assert patch__update_entity_unique_ids.call_count == 0 + + +@pytest.mark.asyncio +async def test_manager_unique_id_update_errors(manager_us_customary_units: Manager, caplog): + manager = manager_us_customary_units + system = manager.api.system_list[0] + system.productType = "S40" + with caplog.at_level(logging.ERROR), patch.object( + manager, "_update_device_unique_ids" + ) as patch_update_device_unique_ids, patch.object( + manager, "_update_entity_unique_ids" + ) as patch__update_entity_unique_ids: + caplog.clear() + patch__update_entity_unique_ids.side_effect = KeyError("this is the error") + await manager.unique_id_updates() + assert patch_update_device_unique_ids.call_count == 1 + assert patch__update_entity_unique_ids.call_count == 1 + + assert len(caplog.messages) == 1 + assert "this is the error" in caplog.messages[0] + assert "Failed to update entity unique_ids" in caplog.messages[0] + + with caplog.at_level(logging.ERROR), patch.object( + manager, "_update_device_unique_ids" + ) as patch_update_device_unique_ids, patch.object( + manager, "_update_entity_unique_ids" + ) as patch_update_entity_unique_ids: + caplog.clear() + patch_update_device_unique_ids.side_effect = KeyError("this is the error") + await manager.unique_id_updates() + assert patch_update_device_unique_ids.call_count == 1 + assert patch_update_entity_unique_ids.call_count == 1 + + assert len(caplog.messages) == 1 + assert "this is the error" in caplog.messages[0] + assert "Failed to update device unique_ids" in caplog.messages[0] + + # There are problems with Event Loops that makes this test fail. Needs fixing. # @pytest.mark.asyncio # async def test_manager_event_wait_mp_wakeup(manager_us_customary_units: Manager, caplog): From 274fa8411e428563b0ae42cfff0b9a2c5a6388f8 Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Sat, 6 May 2023 11:02:00 -0400 Subject: [PATCH 3/9] Create ble_iaq.json --- tests/messages/ble_iaq.json | 2678 +++++++++++++++++++++++++++++++++++ 1 file changed, 2678 insertions(+) create mode 100644 tests/messages/ble_iaq.json diff --git a/tests/messages/ble_iaq.json b/tests/messages/ble_iaq.json new file mode 100644 index 0000000..d98f6a0 --- /dev/null +++ b/tests/messages/ble_iaq.json @@ -0,0 +1,2678 @@ +"messages": [ + { + "MessageId": 0, + "SenderID": "LCC", + "TargetID": "ha_entryway", + "MessageType": "PropertyChange", + "Data": { + "ble": { + "status": { + "state": "normal", + "discoveryStatus": "discoveryCompleted", + "writeAccess": "internal" + }, + "uiRasGroupSettings": { + "writeAccess": "internal" + }, + "szDevices": 4, + "devices": [ + { + "device": { + "deviceName": "s40 1", + "cfStatus": "configured", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "disabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 768, + "zId": 0, + "deviceType": "tstat", + "config": { + "powerType": "linePowered", + "features": [ + { + "id": 0, + "feature": { + "name": "Control Model Number", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "107088-01" + } + ], + "fid": 3000, + "unit": "", + "format": "nts" + } + }, + { + "id": 1, + "feature": { + "name": "Control Serial Number", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "BT22L" + } + ], + "fid": 3001 + } + }, + { + "id": 2, + "feature": { + "name": "Control Hardware Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "A" + } + ], + "fid": 3002 + } + }, + { + "id": 3, + "feature": { + "name": "Control Software Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "04.15.0012" + } + ], + "fid": 3003 + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 4, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "freshInstallation", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "deviceProvAddress": 8194, + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 0 + }, + { + "device": { + "deviceName": "", + "cfStatus": "configured", + "uiRasSettings": { + "senBasedParticpt": "on", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 512, + "zId": 0, + "deviceType": "ras", + "config": { + "powerType": "batteryPowered", + "features": [ + { + "id": 0, + "feature": { + "name": "Control Model Number", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "22V25" + } + ], + "fid": 3000, + "unit": "", + "format": "nts" + } + }, + { + "id": 1, + "feature": { + "name": "Control Serial Number", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "TS23A0" + } + ], + "fid": 3001 + } + }, + { + "id": 2, + "feature": { + "name": "Control Hardware Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "B" + } + ], + "fid": 3002 + } + }, + { + "id": 3, + "feature": { + "name": "Control Software Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "04.00.0481" + } + ], + "fid": 3003 + } + }, + { + "id": 4, + "feature": { + "name": "Node Information", + "format": "uint8", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "49" + } + ], + "fid": 3004 + } + }, + { + "id": 5, + "feature": { + "name": "Control Bootloader Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "01.10.0004" + } + ], + "fid": 3005 + } + } + ], + "parameters": [ + { + "parameter": { + "name": "Equipment Name", + "pid": 2000, + "defaultValue": "\u0010\u00d2W", + "enabled": true, + "szValues": 3, + "value": "RAS", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "Min value", + "string": { + "max": "100" + }, + "format": "nts", + "descriptor": "string" + }, + "id": 0 + }, + { + "parameter": { + "name": "TX Power", + "format": "int8", + "pid": 2001, + "defaultValue": "19", + "enabled": true, + "szValues": 1, + "value": "19", + "descriptor": "range", + "range": { + "max": "20", + "min": "10", + "inc": "1" + }, + "unit": "DBM" + }, + "id": 1 + }, + { + "parameter": { + "name": "Status Update Transmission Frequency", + "format": "uint16", + "pid": 2002, + "defaultValue": "1800", + "enabled": true, + "szValues": 2, + "value": "1800", + "descriptor": "range", + "range": { + "max": "1800", + "min": "5", + "inc": "1" + }, + "unit": "Second" + }, + "id": 2 + }, + { + "parameter": { + "name": "Enable or Disable Diagnostic Data In Device Status Message", + "format": "uint8", + "pid": 2003, + "defaultValue": "0", + "enabled": true, + "szValues": 1, + "value": "0", + "descriptor": "radio", + "radio": { + "max": "1", + "texts": [ + { + "text": "", + "id": 0 + }, + { + "text": "", + "id": 1 + } + ] + }, + "unit": "Min value" + }, + "id": 3 + }, + { + "parameter": { + "name": "Enable or Disable Logging", + "format": "uint8", + "pid": 2004, + "defaultValue": "0", + "enabled": true, + "szValues": 1, + "value": "0", + "descriptor": "radio", + "radio": { + "max": "1", + "texts": [ + { + "text": "", + "id": 0 + }, + { + "text": "", + "id": 1 + } + ] + }, + "unit": "Min value" + }, + "id": 4 + }, + { + "parameter": { + "name": "Low RSSI Threshold Value", + "format": "int8", + "pid": 2005, + "defaultValue": "170", + "enabled": true, + "szValues": 1, + "value": "-86", + "descriptor": "range", + "range": { + "max": "176", + "min": "156", + "inc": "1" + }, + "unit": "DBM" + }, + "id": 5 + }, + { + "parameter": { + "name": "Low RSSI Detection Counter", + "format": "uint8", + "pid": 2006, + "defaultValue": "5", + "enabled": true, + "szValues": 1, + "value": "1", + "descriptor": "range", + "range": { + "max": "10", + "min": "5", + "inc": "1" + }, + "unit": "Min value" + }, + "id": 6 + }, + { + "parameter": { + "name": "Temperature Threshold Value", + "format": "float", + "pid": 2054, + "defaultValue": "0.000000", + "enabled": true, + "szValues": 4, + "value": "0.200000", + "descriptor": "range", + "range": { + "max": "0.030000", + "min": "0.000000", + "inc": "0.000000" + }, + "unit": "Fahrenheit" + }, + "id": 7 + }, + { + "parameter": { + "name": "Humidity publish threshold", + "format": "uint8", + "pid": 2055, + "defaultValue": "1", + "enabled": true, + "szValues": 1, + "value": "1", + "descriptor": "range", + "range": { + "max": "3", + "min": "1", + "inc": "1" + }, + "unit": "Percentage" + }, + "id": 8 + }, + { + "parameter": { + "name": "Occupied detection time value", + "format": "uint32", + "pid": 2056, + "defaultValue": "1800", + "enabled": true, + "szValues": 4, + "value": "1800", + "descriptor": "range", + "range": { + "max": "1800", + "min": "10", + "inc": "60" + }, + "unit": "Second" + }, + "id": 9 + }, + { + "parameter": { + "name": "Minimum counts for motion detection", + "format": "uint32", + "pid": 2057, + "defaultValue": "2", + "enabled": true, + "szValues": 4, + "value": "2", + "descriptor": "range", + "range": { + "max": "5", + "min": "2", + "inc": "1" + }, + "unit": "Min value" + }, + "id": 10 + }, + { + "parameter": { + "name": "Sensor sampling time", + "format": "uint16", + "pid": 2058, + "defaultValue": "120", + "enabled": true, + "szValues": 2, + "value": "120", + "descriptor": "range", + "range": { + "max": "300", + "min": "5", + "inc": "1" + }, + "unit": "Second" + }, + "id": 11 + }, + { + "parameter": { + "name": "Friend poll interval time", + "format": "uint16", + "pid": 2061, + "defaultValue": "120", + "enabled": true, + "szValues": 2, + "value": "120", + "descriptor": "range", + "range": { + "max": "600", + "min": "5", + "inc": "1" + }, + "unit": "Second" + }, + "id": 12 + }, + { + "parameter": { + "name": "Sleep_mode_off", + "format": "uint8", + "pid": 2062, + "defaultValue": "0", + "enabled": true, + "szValues": 1, + "value": "0", + "descriptor": "radio", + "radio": { + "max": "1", + "texts": [ + { + "text": "", + "id": 0 + }, + { + "text": "", + "id": 1 + } + ] + }, + "unit": "Min value" + }, + "id": 13 + } + ], + "szFeatures": 6, + "szParameters": 14, + "writeAccess": "internal", + "notification": { + "installerNote": "freshInstallation", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "deviceProvAddress": 8195, + "devStatus": { + "szStatus": 6, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "rssi", + "vid": 4000, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "-51" + } + ], + "doNotPersist": true, + "unit": "none", + "format": "int8" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + }, + { + "id": 1, + "status": { + "name": "Alarm status", + "vid": 4001, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "1" + } + ], + "unit": "none" + } + }, + { + "id": 2, + "status": { + "name": "Device State", + "vid": 4002, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "1" + } + ], + "unit": "none" + } + }, + { + "id": 3, + "status": { + "name": "Total Powered Time in Secs", + "vid": 4003, + "format": "uint32", + "values": [ + { + "id": 0, + "value": "435075" + } + ], + "unit": "Sec" + } + }, + { + "id": 4, + "status": { + "name": "S40_BLE_RSSI", + "vid": 4004, + "format": "int8", + "values": [ + { + "id": 0, + "value": "-45" + } + ], + "unit": "none" + } + }, + { + "id": 5 + }, + { + "id": 6 + }, + { + "id": 7 + }, + { + "id": 8 + }, + { + "id": 9 + }, + { + "status": { + "name": "RAS Tsense", + "vid": 4050, + "format": "float", + "values": [ + { + "id": 0, + "value": "72.750000" + } + ], + "unit": "Fahreheit" + }, + "id": 10 + }, + { + "status": { + "name": "RAS Tsense status", + "vid": 4051, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 11 + }, + { + "status": { + "name": "RAS humidity %", + "vid": 4052, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "39" + } + ], + "unit": "%" + }, + "id": 12 + }, + { + "status": { + "name": "RAS Humidity Sensor status", + "vid": 4053, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 13 + }, + { + "status": { + "name": "RAS BAT %", + "vid": 4054, + "format": "float", + "values": [ + { + "id": 0, + "value": "100.000000" + } + ], + "unit": "%" + }, + "id": 14 + }, + { + "id": 15, + "status": { + "name": "RAS Bat Status", + "vid": 4055, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + } + }, + { + "status": { + "name": "RAS Occupancy", + "vid": 4056, + "format": "bool8", + "values": [ + { + "id": 0, + "value": "1" + } + ], + "unit": "none" + }, + "id": 16 + }, + { + "id": 17, + "status": { + "name": "RAS Occupancy Status", + "vid": 4057, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + } + }, + { + "status": { + "name": "RAS Digital Temp", + "vid": 4058, + "format": "float", + "values": [ + { + "id": 0, + "value": "75.029999" + } + ], + "unit": "Fahreheit" + }, + "id": 18 + }, + { + "id": 19, + "status": { + "name": "RAS Digital Temp status", + "vid": 4059, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + } + }, + { + "status": { + "name": "RAS Analog Temp", + "vid": 4060, + "format": "float", + "values": [ + { + "id": 0, + "value": "72.750000" + } + ], + "unit": "Fahreheit" + }, + "id": 20 + }, + { + "status": { + "name": "RAS Analog Temp status", + "vid": 4061, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 21 + } + ], + "writeAccess": "internal", + "commStatus": "active" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": true, + "writeAccess": "internal", + "doNotPersist": true, + "temperatureStatus": "inRange" + } + }, + "writeAccess": "internal", + "id": 1 + }, + { + "device": { + "deviceName": "home air quality", + "cfStatus": "configured", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 576, + "zId": 0, + "deviceType": "iaq", + "config": { + "powerType": "linePowered", + "features": [ + { + "id": 0, + "feature": { + "name": "Control Model Number", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "21P02" + } + ], + "fid": 3000, + "unit": "", + "format": "nts" + } + }, + { + "id": 1, + "feature": { + "name": "Control Serial Number", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "22L325" + } + ], + "fid": 3001 + } + }, + { + "id": 2, + "feature": { + "name": "Control Hardware Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "A" + } + ], + "fid": 3002 + } + }, + { + "id": 3, + "feature": { + "name": "Control Software Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "04.00.0334" + } + ], + "fid": 3003 + } + }, + { + "id": 4, + "feature": { + "name": "Node Information", + "format": "uint8", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "5" + } + ], + "fid": 3004 + } + }, + { + "id": 5, + "feature": { + "name": "Control Bootloader Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "01.00.0016" + } + ], + "fid": 3005 + } + }, + { + "id": 6, + "feature": { + "name": "Communication Controller Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "04.00.0052" + } + ], + "fid": 3006 + } + }, + { + "id": 7, + "feature": { + "name": "Communication Controller Bootloader Version", + "format": "nts", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "01.10.0003" + } + ], + "fid": 3007 + } + }, + { + "id": 8, + "feature": { + "name": "Duct Mount Status", + "format": "uint8", + "szValues": 1, + "values": [ + { + "id": 0, + "value": "0" + } + ], + "fid": 3100 + } + } + ], + "parameters": [ + { + "parameter": { + "name": "Equipment Name", + "pid": 2000, + "defaultValue": "\u0015", + "enabled": true, + "szValues": 18, + "value": "Indoor Air Quality", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "Min value", + "string": { + "max": "100" + }, + "format": "nts", + "descriptor": "string" + }, + "id": 0 + }, + { + "parameter": { + "name": "TX Power", + "format": "int8", + "pid": 2001, + "defaultValue": "19", + "enabled": true, + "szValues": 1, + "value": "19", + "descriptor": "range", + "range": { + "max": "20", + "min": "10", + "inc": "1" + }, + "unit": "Min value" + }, + "id": 1 + }, + { + "parameter": { + "name": "Enable or Disable Diagnostic Data In Device Status Message", + "format": "uint8", + "pid": 2003, + "defaultValue": "0", + "enabled": true, + "szValues": 1, + "value": "0", + "descriptor": "radio", + "radio": { + "max": "1", + "texts": [ + { + "text": "", + "id": 0 + }, + { + "text": "", + "id": 1 + } + ] + }, + "unit": "Min value" + }, + "id": 2 + }, + { + "parameter": { + "name": "Enable or Disable Logging", + "format": "uint8", + "pid": 2004, + "defaultValue": "0", + "enabled": true, + "szValues": 1, + "value": "0", + "descriptor": "radio", + "radio": { + "max": "1", + "texts": [ + { + "text": "", + "id": 0 + }, + { + "text": "", + "id": 1 + } + ] + }, + "unit": "Min value" + }, + "id": 3 + }, + { + "parameter": { + "name": "Low RSSI Threshold Value", + "format": "int8", + "pid": 2005, + "defaultValue": "170", + "enabled": true, + "szValues": 1, + "value": "-86", + "descriptor": "range", + "range": { + "max": "176", + "min": "156", + "inc": "1" + }, + "unit": "DBM" + }, + "id": 4 + }, + { + "parameter": { + "name": "Low RSSI Detection Counter", + "format": "uint8", + "pid": 2006, + "defaultValue": "5", + "enabled": true, + "szValues": 1, + "value": "1", + "descriptor": "range", + "range": { + "max": "10", + "min": "5", + "inc": "1" + }, + "unit": "Min value" + }, + "id": 5 + } + ], + "szFeatures": 9, + "szParameters": 6, + "writeAccess": "internal", + "notification": { + "installerNote": "freshInstallation", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 12, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "rssi", + "vid": 4000, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "-40" + } + ], + "doNotPersist": true, + "unit": "none", + "format": "int8" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + }, + { + "id": 1, + "status": { + "name": "Alarm status", + "vid": 4001, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + } + }, + { + "id": 2, + "status": { + "name": "Device State", + "vid": 4002, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "1" + } + ], + "unit": "none" + } + }, + { + "id": 3, + "status": { + "name": "Total Powered Time in Secs", + "vid": 4003, + "format": "uint32", + "values": [ + { + "id": 0, + "value": "109" + } + ], + "unit": "Sec" + } + }, + { + "id": 4, + "status": { + "name": "S40_BLE_RSSI", + "vid": 4004, + "format": "int8", + "values": [ + { + "id": 0, + "value": "-38" + } + ], + "unit": "none" + } + }, + { + "id": 5 + }, + { + "id": 6 + }, + { + "id": 7 + }, + { + "id": 8 + }, + { + "id": 9 + }, + { + "status": { + "name": "IAQ PM2_5", + "vid": 4100, + "format": "uint16", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 10 + }, + { + "id": 11 + }, + { + "status": { + "name": "IAQ PM2_5 Status", + "vid": 4102, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 12 + }, + { + "status": { + "name": "IAQ CO2", + "vid": 4103, + "format": "uint16", + "values": [ + { + "id": 0, + "value": "668" + } + ], + "unit": "none" + }, + "id": 13 + }, + { + "status": { + "name": "IAQ CO2 Status", + "vid": 4104, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 14 + }, + { + "status": { + "name": "IAQ VOC", + "vid": 4105, + "format": "uint16", + "values": [ + { + "id": 0, + "value": "1250" + } + ], + "unit": "none" + }, + "id": 15 + }, + { + "status": { + "name": "IAQ VOC Status", + "vid": 4106, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 16 + }, + { + "status": { + "name": "IDLE Switch Status", + "vid": 4107, + "format": "uint8", + "values": [ + { + "id": 0, + "value": "0" + } + ], + "unit": "none" + }, + "id": 17 + } + ], + "writeAccess": "internal", + "commStatus": "active" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + }, + "deviceProvAddress": 8196 + }, + "writeAccess": "internal", + "id": 2 + }, + { + "device": { + "deviceName": "", + "cfStatus": "unKnown", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 0, + "zId": 0, + "deviceType": "unKnown", + "config": { + "powerType": "unKnown", + "features": [ + { + "id": 0, + "feature": { + "name": "", + "szValues": 0, + "values": [ + { + "id": 0, + "value": "" + } + ], + "fid": 0, + "unit": "" + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 0, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "unknown", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 3 + }, + { + "device": { + "deviceName": "", + "cfStatus": "unKnown", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 0, + "zId": 0, + "deviceType": "unKnown", + "config": { + "powerType": "unKnown", + "features": [ + { + "id": 0, + "feature": { + "name": "", + "szValues": 0, + "values": [ + { + "id": 0, + "value": "" + } + ], + "fid": 0, + "unit": "" + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 0, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "unknown", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 4 + }, + { + "device": { + "deviceName": "", + "cfStatus": "unKnown", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 0, + "zId": 0, + "deviceType": "unKnown", + "config": { + "powerType": "unKnown", + "features": [ + { + "id": 0, + "feature": { + "name": "", + "szValues": 0, + "values": [ + { + "id": 0, + "value": "" + } + ], + "fid": 0, + "unit": "" + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 0, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "unknown", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 5 + }, + { + "device": { + "deviceName": "", + "cfStatus": "unKnown", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 0, + "zId": 0, + "deviceType": "unKnown", + "config": { + "powerType": "unKnown", + "features": [ + { + "id": 0, + "feature": { + "name": "", + "szValues": 0, + "values": [ + { + "id": 0, + "value": "" + } + ], + "fid": 0, + "unit": "" + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 0, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "unknown", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 6 + }, + { + "device": { + "deviceName": "", + "cfStatus": "unKnown", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 0, + "zId": 0, + "deviceType": "unKnown", + "config": { + "powerType": "unKnown", + "features": [ + { + "id": 0, + "feature": { + "name": "", + "szValues": 0, + "values": [ + { + "id": 0, + "value": "" + } + ], + "fid": 0, + "unit": "" + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 0, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "unknown", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 7 + }, + { + "device": { + "deviceName": "", + "cfStatus": "unKnown", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 0, + "zId": 0, + "deviceType": "unKnown", + "config": { + "powerType": "unKnown", + "features": [ + { + "id": 0, + "feature": { + "name": "", + "szValues": 0, + "values": [ + { + "id": 0, + "value": "" + } + ], + "fid": 0, + "unit": "" + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 0, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "unknown", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 8 + }, + { + "device": { + "deviceName": "", + "cfStatus": "unKnown", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 0, + "zId": 0, + "deviceType": "unKnown", + "config": { + "powerType": "unKnown", + "features": [ + { + "id": 0, + "feature": { + "name": "", + "szValues": 0, + "values": [ + { + "id": 0, + "value": "" + } + ], + "fid": 0, + "unit": "" + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 0, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "unknown", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 9 + }, + { + "device": { + "deviceName": "", + "cfStatus": "unKnown", + "uiRasSettings": { + "senBasedParticpt": "off", + "enableState": "enabled", + "writeAccess": "internal", + "sleepSetting": { + "scheduleFrom": "", + "writeAccess": "openAll", + "sleep": "off", + "scheduleTill": "" + } + }, + "wdn": 0, + "zId": 0, + "deviceType": "unKnown", + "config": { + "powerType": "unKnown", + "features": [ + { + "id": 0, + "feature": { + "name": "", + "szValues": 0, + "values": [ + { + "id": 0, + "value": "" + } + ], + "fid": 0, + "unit": "" + } + } + ], + "parameters": [ + { + "parameter": { + "name": "", + "pid": 0, + "defaultValue": "", + "enabled": false, + "szValues": 0, + "value": "", + "range": { + "max": "", + "min": "", + "inc": "" + }, + "radio": { + "max": "", + "texts": [ + { + "text": "", + "id": 0 + } + ] + }, + "unit": "", + "string": { + "max": "" + } + }, + "id": 0 + } + ], + "szFeatures": 0, + "szParameters": 0, + "writeAccess": "internal", + "notification": { + "installerNote": "unknown", + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "devStatus": { + "szStatus": 0, + "doNotPersist": true, + "inputsStatus": [ + { + "status": { + "name": "", + "vid": 0, + "szValues": 0, + "writeAccess": "internal", + "values": [ + { + "id": 0, + "value": "" + } + ], + "doNotPersist": true, + "unit": "" + }, + "doNotPersist": true, + "writeAccess": "internal", + "id": 0 + } + ], + "writeAccess": "internal", + "commStatus": "unKnown" + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "rasStatus": { + "isParticipating": false, + "writeAccess": "internal", + "doNotPersist": true + } + }, + "writeAccess": "internal", + "id": 10 + } + ], + "bleGwController": { + "writeAccess": "internal", + "command": "updateFirmware 0x300 BT22L /lcc/data/staged/BigBendBLE/S40_BLE/S40_BLE.gbl 433896", + "publisher": { + "writeAccess": "openAll", + "publisherName": "unknown", + "doNotPersist": true + } + }, + "publisher": { + "writeAccess": "openAll", + "publisherName": "unknown", + "doNotPersist": true + }, + "userEdited": { + "writeAccess": "openAll", + "iaq": { + "writeAccess": "openAll", + "doNotPersist": true + }, + "ras": { + "scheduleFrom": "", + "doNotPersist": true, + "enableState": "unknown", + "senBasedParticpt": "unknown", + "writeAccess": "openAll", + "sleep": "off", + "followMe": "unknown", + "scheduleTill": "" + }, + "doNotPersist": true + }, + "command": { + "writeAccess": "remote", + "request": { + "toOne": { + "deviceName": "", + "uniqueIdentifier": 600, + "doNotPersist": true, + "cmdAndData": { + "cmd": { + "requestType": "provCompleted", + "writeAccess": "remote", + "doNotPersist": true + }, + "writeAccess": "remote", + "update": { + "doNotPersist": true, + "pid": 0, + "value": "", + "writeAccess": "remote" + }, + "doNotPersist": true + }, + "writeAccess": "remote" + }, + "toAll": { + "cmdAndData": { + "cmd": { + "requestType": "unKnown", + "writeAccess": "remote", + "doNotPersist": true + }, + "writeAccess": "remote", + "update": { + "doNotPersist": true, + "pid": 0, + "value": "", + "writeAccess": "remote" + }, + "doNotPersist": true + }, + "writeAccess": "remote", + "doNotPersist": true + }, + "doNotPersist": true, + "toGroup": { + "cmdAndData": { + "cmd": { + "requestType": "unKnown", + "writeAccess": "remote", + "doNotPersist": true + }, + "writeAccess": "remote", + "update": { + "doNotPersist": true, + "pid": 0, + "value": "", + "writeAccess": "remote" + }, + "doNotPersist": true + }, + "targetGroup": "unKnown", + "writeAccess": "remote", + "doNotPersist": true + }, + "writeAccess": "remote" + }, + "response": { + "deviceName": "", + "uniqueIdentifier": 0, + "doNotPersist": true, + "ack": { + "resp": "unKnown", + "writeAccess": "remote", + "doNotPersist": true + }, + "cmd": { + "requestType": "unKnown", + "writeAccess": "remote", + "doNotPersist": true + }, + "update": { + "doNotPersist": true, + "pid": 0, + "value": "", + "writeAccess": "remote" + }, + "writeAccess": "remote" + }, + "doNotPersist": true + }, + "internalRequest": { + "deviceName": "", + "uniqueIdentifier": 0, + "doNotPersist": true, + "requestType": "unKnown", + "writeAccess": "local" + } + } + } + } +] +} From f517106be8f8eff31b95717bf88976111a91b2cc Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Sun, 7 May 2023 20:33:13 -0400 Subject: [PATCH 4/9] Add additional sensors for air quaility measurement lta, sta --- .coverage | Bin 69632 -> 69632 bytes coverage.xml | 1229 ++++++++++------- .../lennoxs30/ble_device_21p02.py | 81 +- custom_components/lennoxs30/sensor.py | 8 +- custom_components/lennoxs30/sensor_iaq.py | 120 ++ tests/conftest.py | 3 + .../system_04_furn_ac_zoning_ble.json | 2 +- ...em_04_furn_ac_zoning_indoorAirQuality.json | 73 + tests/test_sensor_iaq.py | 105 ++ tests/test_sensor_setup.py | 9 +- 10 files changed, 1112 insertions(+), 518 deletions(-) create mode 100644 custom_components/lennoxs30/sensor_iaq.py create mode 100644 tests/messages/system_04_furn_ac_zoning_indoorAirQuality.json create mode 100644 tests/test_sensor_iaq.py diff --git a/.coverage b/.coverage index c80d3d75d38133d7de78746a7563e34ae369efee..f17ea83428f7f708ec4c577b8041780dd003c43a 100644 GIT binary patch delta 3326 zcmYjT2~-s49iN$oPO_W`MlxG4p-D|L=c) z`=8x+65n?czd+V$FCg&6Fc&VF!`@|38ay$+295q`8=lP85?2r=$edx`VG5XZ`T>2L z9-|M^uh2QPg~m)%rgNsFroEDV_jTUG-w410=YJlpc+Nn)c85KuqNiX>&IY6E#-z2-p-K6Ph zGK;j38sa{|5mzoc^9eS-7DshuwfkC8PV#qfveecgg!oQ&fSnaZf~kSh4dn7| z6Rnil0fBVKYirSm9!L&xG8$%wP^ul-82xanlMl=Y2@%y0$*e~DT7sfDBe9(d_-0hgf*%_UG>HxioR#tFu*H z0LC}OZ?eQ>L9igJo3@{>8|CEbkyeja8uR>5O0X<}lI$hj=eCRr@}toXzwB$;@}Zetd4dyVo*nZ3_+~wOdBX@No%MKlekK$=c4_Rxv#lH#@)wV8 zZ`)GuYZin7*Wu}B#zyGu0d#aSGq)w&$UECC_A$M==Db;FJ6N7;LkuLq9P!i6LF4p zyE*=ofM1gQJTFN!6Atm@@P0{@I-%$YOIgf{0W+4p4@h#A_>;62w(-}enfNdWmxXor zbnLu5_MZP^*R|u*!q%-@#yb4P0`Fjya^g&PN1g5DlU!e$%*iK)Qrar38}GSY*KYWp z>hrrMzJmAqm$^p&-Ra5xRzXx2@1J!YzHHmzYtqm(^l<6n;c}E}!4-4cK-rO!LjJOF z=RCV*dV@SRQZI{q8$aFbz9j|SlK9>WqP(nntL)~-K9ri91#yIJm1KdOXc&YBh*SQ=?oQ@N&s@eKpEjkVO0og!w6BH93t> z7&V3r!;9MYDHlc1|D{hE-1rydK4OGeP4t*<(A$`QGVjvyrVXYTLS0TUyuh^$q37XyYzON>o>nqS=d{Pv&a zwCr61PTed8^`kl>le~)TayoZFWe1T#twS76rwu$gwwg$zWTeUId;w_xRYN3e?;;;K zou^eFxi@q~Jc6J*E;_fN;P`kQ8U>u51Tgrm0pyL-AbrA&Tgo+U;M915;o03gp~J|v zcvl>Zq6mCAVZ`%W@(M0BO}vG#&!CbJrjL1^Sqh)G@6j!^jV4W>n|e%}O;+Or;{{`j z(Pj`07Y$8@6#X6jY5g|+BHcH-6S@Xn0erSz)pl#wXld#*>P>1hl>|F=jRZb#1zuaC z*#po!8KCYqgVum9-(v9iZ3^_<)}juWyArDr4Bn2#?G>61NL&iElQdeb67Nq^Ao*Jz z*rw9LLC2&Sw-ss{6&f{Hze;dqvJ|ga0OxWnSG3=lVu-R@yvqRD*`RtV5-nAYwoftW z3UKkxO0)!CGeMFs0WAjZi3D&V5Ds?xBGDqqD<%rnLfL&wffq8CtFcx;hyY;^!qEcw zcO?_F+)YFC)!-D(6A!{Q`I-h;>U1!BS5M?t;a#Y@%e@zk=BOT&zBJ#9!~p zpo#N^Gk_eP0*$;GO_;Cv9CD72z~d@4EVNT#+OzYe))sg^7Olg(AZ@$=+z&<*v5(+m?uxTZ(529vN)u>DviDxiaC8(NDp$Ov-MX-_1US89+( z;GD_;)suu_uFWedG|0;f7`QxXAeNVZkENaKdr=u+A6nm-3WKdSPu0RU!!2ls`x0gQ56^u46@oD*J0619H)}M_N8O?0`TvEgUE+q ztO~;BV^w)zyEJD3b|1|J7a!!~Igp}u2DTk@EK_~V_Z_omLo4wh`Kx%)JFQ2TLIo@M zeA+-{K_v#-jZtI~dv>Z5VlJtbOTI5-=4Eqbv1fi~uYN|4FV-N<;L5#xEYGT1hGz(~ zXDm<*dv@;L`P@se$JOb;EZEUBz(1M`=8(YPsfzL`e>yngH(|voU|1+YlVS8LjL+?U zzH$3Y%J9fbQtiTHh7~2kauvf9rBp!4Z~ly7_V{^+2_P@1L*w9Xu_}beji^|pL0GkZ za6HiqXKrmQOfg{rh6gW(h=EAN9P%iIh=#klvMW>V^+!=~&YgHvhgx6;tMNz;(zc*% zoGaRz2&kcSPbp{uI5T0GA9bO(@NkOWj4*EI0JDbC(!=y`>CJFlt)hu?_+aT!482{- zpiyvG4GUH{tirG&hk7*T=N4wSt2w!EFCxmT6_xq3s!T4ds7mF|k1o_UUmtaN{T~k3 BZoU8j delta 2912 zcmZ8j3se;66`p^0Ui+AtUEbg>FL{F?AjosqMH6i_3ay^xXd|c)qzPbDLJW@ubE2s> zk@e51Cou+X(l(}5G=$PXU`-$A#I%v5JxGMEo1{%QvbYo2zyQn6^q++$J>A`N_Ws}f z{(Haw-kCeoaS`vhh+iYi>8lv}aNN~iz3vT;U?0XijZpt}0@O{gu$vDD5${ytu~rMo zVB7-tTkcIRmrG=4*?a6~>}j@z&0@n?CF5nTF=v@p<_TseV_`BF1Ebc?YroKbtnJla z)V{2_sj1ZLQ2$2Vpsr9GRAZ{Esx7KaRV00r?xnkEE%hxmO1(pMQHQ7s%0w9{HTf<1 z75Nd_PhKLQCijul4zh$yCze)K#c&qiGSNio%?L;^pV$WG9f<66N~JHMQW9mK~lH`?K(umXVZI+hIl3f}S8bgW^H?5eh zu1K-l?R-zE-Pz-`>WdJXP!tGT&r2?0%p6&Wpml`HJ_t_=CpU-j&5>tR& zhJwu0JAU^aKM}HC4!IsJ1v}s36Q)E_YzO+td^u+RgJE6WR&fLg@k%jy2-=VbJttNb ztdqU#u>Kog`J_3~>l-#1a}iXMi}jySbdR{a3rqFGyiYnW`OtUCqlnHyZc`4loQxu~ zky(}v_nkECUyIC5Yq9iHynXLp6WZ8(*C+YW7D3_{MtFzgF=Lh-Hw)9;)ID=-!G+W; zxO~H3neB)oNt&=sbPPJbe?P_GvqWGaNi3? zV>&X^(iNY6m3-FiX^7luNQ38J_<=PQF;Y^aJDy)MJH6=KBmC~rh!h0o zq#S20zbe1&lx9bp94>Lp@o&*soh)zNl6;(K)!q0!&EMt}y;k3#XJJ%#9Apzw{x8gg}q{ZeD>6X;)^9z;Yg3#tfKQG_Lk65Jz-n=nU z)gA zSi~v><*ZWp^B)}+Zw>TcmG;y+_B`V2XM>`UCn@R|e-4^&Z69_@zVrOC&`5-3L=JQj zAKsNj|A-`s!d;2fMIbmnBIwG8lF%wRn)$uDa2W|lh;;=TX2TE^9k&0aJLp>R3}yv~ zqIF7W(81>=Ps51d^SAR7(1#!}A!J-tD>T|m2mMYyD>N8E8NmZxw-)`pzZb=`qnm@^ zf)E@VG$8uLcDvnqh6>RmAWg5x=zyYR_CNklaXjte7z4DuXxwPwm2>P#gwDrH&JN9e$ZHEvre&@-bTikhNkm;RyaRZNLcK59+s9WCGkgnq7-(qM{)}8T~dF)Pn@*p z`r3NlhHsuJi`zZ?zRPD71+&>Kna$nZBO??6aFvQxsL|B5u*Hz-dJawA>r8L)>o?<+ z7IqL)H)JH6?H#f7ZaiVQhQ#LulA@fpm9s922`!8Lb{J`r?*j`P_I%t@;q5XTqff2FY#-5 zBfeVsxe_XWtc+0nP4P3uE`<)<0dInzfK+S-1AR7IE>2v=uV1%a!C}YkLZ}&45ou%% zmSeMJ!p)-(f)se^ZXxU#CF#sV$C4=>cF<<4M$|GQfqE4yv)N3L7~8Cjr?5Du@qQvwg5$;wa=2Fu6GK`6X5z8McGR<^8A zLwcW@fcTbp%8hY<;H;d9qu5dQ0$a-_qQ}t?bDG)AP}))L%i1kkLi2k~r>0absIRM^ zRp+Th)lJnI)pk`lJx-satLPNUjY{54!5CKwZdtE9f^sD4n(K}pI9T~?V?$j-Ejrl- zc*v6s2VER6qXQe^HCGT^bOi$w+Le6n8V4v?@-+)eWtzO?Knbi0@U$@Y9(j)EfF3U{ zR5qjF>D=l)o0ONu?7_3Eg-3qc+R4wBf zQ+kjIO;g!m4Lmeu1{o;kMm#-RSsSQx$*dZr!cXoeL-QPqC#}Gm>YAtP4%8e$;uGOZ z_bp&GqU7Oma^)U4Y*9#yKe2J*>o z9aTI{M>6Q@D8=4|)Hg}H^3?a$uc^z`liR0rQfC7=&dwGjZm+kHwKRUEBPu} zO~w)9#9KrS5r=<)_uvh9ywa&WtK6duSA42Ct=OsHz-QnMPzO@5Y3yB_Er%#BRbp+h zb2?4#9FbR|#GaB@>i^0&7oYn8wVNm?Qew@rhxwnjboaIJ&Qz4b zl7@m6K#kl=M297_8g$Q|nKjeBk3K!01}o=Pz_5I-uK7r?N|vkH-*oh7V}mR&3ayuGfi4wANhDmm zPzoYo!@|QvI7%OkVkE%N`547RVW>pOfx%I<7=}hh#bP-T5>U8IVc^(e5FV_=T2}Tj zFxp4;9`>EZwK%88xFPNYx1H0YVE{NqvE|%e7L?!{ - + @@ -14,7 +14,7 @@ - + @@ -215,51 +215,51 @@ - + - + - + - + - - - + + + - - + + - + - - - - + + + + - - - - + + + + @@ -270,19 +270,19 @@ - - + + - - - + + + - + - - + + @@ -291,7 +291,7 @@ - + @@ -301,25 +301,25 @@ - + - + - + - + - - + + @@ -327,25 +327,25 @@ - + - + - + - - + + - + - + @@ -353,25 +353,25 @@ - - + + - + - - - - - + + + + + - - + + @@ -382,24 +382,24 @@ - + - + - - + + - - + + - - + + @@ -410,22 +410,22 @@ - + - + - - + + - - - - + + + + @@ -436,32 +436,32 @@ - + - + - + - + - + - + - - - + + + @@ -470,31 +470,32 @@ - + - + - + - + - + - + - - - - - - + + + + + + + @@ -539,7 +540,7 @@ - + @@ -555,191 +556,192 @@ - - - - - - + + + + + + - + - + - + - + - + + + + + + - - - - - + + + + + - - - - - - + + + + - + + - - - + - - - - - + + + + + - + - + - - + + - + - + + + - - + + - - - + + + + - - - - + - + - + - + - + + + - - - - + + + + - - - + + + + - - - - + - + - + - + - + + + - - - - + + + + - - - + + + + - - - - + - + - + - @@ -748,66 +750,71 @@ - + + + + - - - - + + + + - - - + + + + - - - - + - + - + - + - - - - + + + + - + + + + - - - - - - + + + - + - + + + + + @@ -919,6 +926,17 @@ + + + + + + + + + + + @@ -1890,7 +1908,7 @@ - + @@ -1932,30 +1950,32 @@ - - - + - - - + + + - - + + + - - - + + - - + + + + + + @@ -2486,7 +2506,7 @@ - + @@ -2498,425 +2518,442 @@ - - - - + + + + - - + + + - - + - + + - - - + + - - - - + + + - - + + + - + - + - + + + + + - + - - - - + + + + + - - - - + + + + + - - - - - - + + + + - - + + - - - - - - + + - + + + + + + - - - - + - - - - + - + + - + + + + + + + + + + + + + + + - - - - - - - - + - - + - + - - - - + + - - + + - - + - + + - - - + + + + - + + + + + + + - - - - - + - - - - + - - + + + + - + - - + + + - - - - - + + - - - + + + + - + + + + + + + - - - - - + - - - - + - - + + - + - - + + + - - + + + + - - - - + - + + - - + + - + + + - + + - - - - + + - - - - + - - + + - - + + - + + - - + + + + + - - + - - - - + + + + + + + + - - - - - + - - - - - + + - + - - + + - - - + + + + + + + + - + + - - - + + + + - - - + + - - - - - - - - - - + + - + - + + + + + + + + + + + + + + + + + @@ -3004,6 +3041,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3399,13 +3515,13 @@ - + - + @@ -3599,25 +3715,24 @@ + - - + + - - - + + + - - + - @@ -3627,9 +3742,9 @@ + + - - @@ -3637,9 +3752,9 @@ + - @@ -3647,23 +3762,23 @@ + + - - - - + + - - + + + + - - @@ -3675,45 +3790,48 @@ + + - - - - + + - - + + + + - - - - + + - - + + + + - - - - + + - - + + + + + @@ -4811,36 +4929,40 @@ + - + - + - + - + + + + @@ -7750,113 +7872,112 @@ - - - - - - - + + + + + + - - + + - + - + - + + - - + - + + - - + - + + - - + - + - + + + - - - + + - - + @@ -7896,19 +8017,19 @@ + - - + - + @@ -7917,49 +8038,54 @@ - + - + + - - + - + + - - + - + + + + + + @@ -10535,6 +10661,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -10635,14 +10850,14 @@ - - + + - + - + @@ -10650,7 +10865,7 @@ - + @@ -10661,17 +10876,17 @@ - + - + - + @@ -10679,41 +10894,41 @@ - + - + - + - + - + - + - + @@ -10729,7 +10944,7 @@ - + @@ -10744,7 +10959,7 @@ - + @@ -10759,17 +10974,17 @@ - + - + - + @@ -10777,17 +10992,17 @@ - + - + - + @@ -10795,7 +11010,7 @@ - + @@ -10807,7 +11022,7 @@ - + @@ -10821,7 +11036,7 @@ - + @@ -10831,15 +11046,15 @@ - + - + - + @@ -10847,8 +11062,10 @@ + - + + diff --git a/custom_components/lennoxs30/ble_device_21p02.py b/custom_components/lennoxs30/ble_device_21p02.py index f6b1e8b..fc3bd96 100644 --- a/custom_components/lennoxs30/ble_device_21p02.py +++ b/custom_components/lennoxs30/ble_device_21p02.py @@ -2,7 +2,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, CONCENTRATION_PARTS_PER_MILLION +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT lennox_21p02_sensors = [ { @@ -31,10 +31,9 @@ { "input_id": 4100, "status_id": 4102, - "name": "pm2_5", + "name": "pm25", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.PM25, - "uom": CONCENTRATION_PARTS_PER_MILLION, }, { "input_id": 4103, @@ -49,7 +48,6 @@ "name": "voc", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - "uom": CONCENTRATION_PARTS_PER_MILLION, }, ] @@ -58,3 +56,78 @@ {"input_id": 4002, "name": "device_state", "entity_category": EntityCategory.DIAGNOSTIC}, {"input_id": 4107, "name": "idle_switch", "entity_category": EntityCategory.DIAGNOSTIC}, ] + +lennox_iaq_sensors = [ + { + "input": "iaq_mitigation_action", + "name": "iaq mitigation action", + }, + { + "input": "iaq_mitigation_state", + "name": "iaq mitigation state", + }, + { + "input": "iaq_overall_index", + "name": "iaq overall index", + }, + { + "input": "iaq_pm25_sta", + "status": "iaq_pm25_sta_valid", + "name": "iaq pm25 sta", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.PM25, + "precision": 4, + }, + { + "input": "iaq_pm25_lta", + "status": "iaq_pm25_lta_valid", + "name": "iaq pm25 lta", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.PM25, + "precision": 4, + }, + { + "input": "iaq_pm25_component_score", + "name": "iaq pm25 component score", + }, + { + "input": "iaq_voc_sta", + "status": "iaq_voc_sta_valid", + "name": "iaq voc sta", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + "precision": 4, + }, + { + "input": "iaq_voc_lta", + "status": "iaq_voc_lta_valid", + "name": "iaq voc lta", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + "precision": 4, + }, + { + "input": "iaq_voc_component_score", + "name": "iaq voc component score", + }, + { + "input": "iaq_co2_lta", + "status": "iaq_co2_lta_valid", + "name": "iaq co2 lta", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.CO2, + "precision": 1, + }, + { + "input": "iaq_co2_sta", + "status": "iaq_co2_sta_valid", + "name": "iaq co2 sta", + "state_class": SensorStateClass.MEASUREMENT, + "device_class": SensorDeviceClass.CO2, + "precision": 1, + }, + { + "input": "iaq_co2_component_score", + "name": "iaq co2 component score", + }, +] diff --git a/custom_components/lennoxs30/sensor.py b/custom_components/lennoxs30/sensor.py index bd1f0c3..f5794e7 100644 --- a/custom_components/lennoxs30/sensor.py +++ b/custom_components/lennoxs30/sensor.py @@ -34,7 +34,7 @@ LENNOX_STATUS_NOT_EXIST, ) - +from . import Manager from .base_entity import S30BaseEntityMixin from .const import ( MANAGER, @@ -44,10 +44,10 @@ ) from .helpers import helper_create_system_unique_id, helper_get_equipment_device_info, lennox_uom_to_ha_uom from .ble_device_22v25 import lennox_22v25_sensors -from .ble_device_21p02 import lennox_21p02_sensors +from .ble_device_21p02 import lennox_21p02_sensors, lennox_iaq_sensors from .sensor_ble import S40BleSensor +from .sensor_iaq import S40IAQSensor -from . import Manager _LOGGER = logging.getLogger(__name__) @@ -124,6 +124,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e if ble_device.controlModelNumber == "22V25": ble_sensors = lennox_22v25_sensors elif ble_device.controlModelNumber == "21P02": + for sensor_item in lennox_iaq_sensors: + sensor_list.append(S40IAQSensor(hass, manager, system, ble_device, sensor_item)) ble_sensors = lennox_21p02_sensors if ble_sensors: for sensor_dict in ble_sensors: diff --git a/custom_components/lennoxs30/sensor_iaq.py b/custom_components/lennoxs30/sensor_iaq.py new file mode 100644 index 0000000..f93717e --- /dev/null +++ b/custom_components/lennoxs30/sensor_iaq.py @@ -0,0 +1,120 @@ +"""Support for Lennoxs30 outdoor temperature sensor""" +# pylint: disable=global-statement +# pylint: disable=broad-except +# pylint: disable=unused-argument +# pylint: disable=line-too-long +# pylint: disable=invalid-name +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.components.sensor import SensorEntity + +from lennoxs30api import lennox_system, LennoxBle + +from . import Manager +from .base_entity import S30BaseEntityMixin +from .const import LENNOX_DOMAIN, UNIQUE_ID_SUFFIX_BLE +from .device import helper_create_ble_device_id +from .helpers import helper_create_system_unique_id + +_LOGGER = logging.getLogger(__name__) + + +class S40IAQSensor(S30BaseEntityMixin, SensorEntity): + """Class for Lennox S40 BLE Sensors.""" + + def __init__( + self, + hass: HomeAssistant, + manager: Manager, + system: lennox_system, + ble_device: LennoxBle, + sensor_dict: dict, + ): + super().__init__(manager, system) + self._hass: HomeAssistant = hass + self._ble_device = ble_device + self._myname: str = self._system.name + " " + ble_device.deviceName + " " + sensor_dict["name"] + self._sensor_dict: dict = sensor_dict + self._system_attr: str = sensor_dict["input"] + self._status_attr: str = sensor_dict.get("status") + self._uom: str = sensor_dict.get("uom", None) + self._state_class: str = sensor_dict.get("state_class", None) + self._device_class: str = sensor_dict.get("device_class", None) + self._entity_category: str = sensor_dict.get("entity_category", None) + self._precision: int = sensor_dict.get("precision", 1) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + _LOGGER.debug("async_added_to_hass S40IAQSensor myname [%s]", self._myname) + attribs = [] + attribs.append(self._system_attr) + if self._status_attr is not None: + attribs.append(self._status_attr) + + self._system.registerOnUpdateCallback(self.sensor_value_update, attribs) + await super().async_added_to_hass() + + def sensor_value_update(self): + """Callback to execute on data change""" + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("sensor_value_update S40IAQSensor myname [%s]", self._myname) + self.schedule_update_ha_state() + + @property + def unique_id(self) -> str: + return helper_create_system_unique_id( + self._system, + f"{UNIQUE_ID_SUFFIX_BLE}_{self._ble_device.ble_id}_{self._system_attr}", + ) + + @property + def name(self): + return self._myname + + @property + def native_value(self): + value = getattr(self._system, self._system_attr) + if self._state_class is None: + return value + try: + return round(float(value), self._precision) + except ValueError as e: + _LOGGER.warning( + "native_value myname [%s] sensor value [%s] exception: [%s]", + self._myname, + value, + e, + ) + return None + + @property + def state_class(self): + return self._state_class + + @property + def device_class(self): + return self._device_class + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return { + "identifiers": {(LENNOX_DOMAIN, helper_create_ble_device_id(self._system, self._ble_device))}, + } + + @property + def native_unit_of_measurement(self): + return self._uom + + @property + def available(self) -> bool: + if self._status_attr is not None: + if getattr(self._system, self._status_attr) is not True: + return False + return super().available + + @property + def entity_category(self): + return self._entity_category diff --git a/tests/conftest.py b/tests/conftest.py index e1ad0fd..6cada1a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -403,6 +403,9 @@ def manager_system_04_furn_ac_zoning(hass) -> Manager: data = loadfile("device_response_lcc.json", "0000000-0000-0000-0000-000000000001") api.processMessage(data) + data = loadfile("system_04_furn_ac_zoning_indoorAirQuality.json", "0000000-0000-0000-0000-000000000001") + api.processMessage(data) + return manager_to_return diff --git a/tests/messages/system_04_furn_ac_zoning_ble.json b/tests/messages/system_04_furn_ac_zoning_ble.json index 61cda66..5c3aea8 100644 --- a/tests/messages/system_04_furn_ac_zoning_ble.json +++ b/tests/messages/system_04_furn_ac_zoning_ble.json @@ -1718,7 +1718,7 @@ "values": [ { "id": 0, - "value": "22L325" + "value": "1234567" } ], "fid": 3001 diff --git a/tests/messages/system_04_furn_ac_zoning_indoorAirQuality.json b/tests/messages/system_04_furn_ac_zoning_indoorAirQuality.json new file mode 100644 index 0000000..3d6ef87 --- /dev/null +++ b/tests/messages/system_04_furn_ac_zoning_indoorAirQuality.json @@ -0,0 +1,73 @@ +{ + "MessageId": 0, + "SenderID": "LCC", + "TargetID": "ha_entryway", + "MessageType": "PropertyChange", + "Data": { + "indoorAirQuality": { + "mitigation_action": "Filtration", + "error_cause": "None", + "required_cleaning_cfm": 0, + "mitigation_state": "State_Paused", + "holdoff_time": 0, + "clear_persistent_error": false, + "overall_index": "Fair", + "writeAccess": "openAll", + "holdoff_status": "None", + "holdoff_request": "None", + "cleanliness_level": "Basic", + "holdoff_time_selected": 0, + "sensor": [ + { + "enable_display": true, + "sta": 0, + "trending_score_validNumber": true, + "scaling_factor": 1, + "component_score": "Good", + "value": 0, + "lta": 0.186265, + "trending_score": -5.1e-05, + "sta_validNumber": true, + "id": 0, + "lta_validNumber": true, + "name": "PM25" + }, + { + "enable_display": true, + "sta": 1299.726944, + "trending_score_validNumber": true, + "component_score": "Fair", + "value": 1250, + "id": 1, + "lta": 297.103471, + "lta_validNumber": true, + "sta_validNumber": true, + "scaling_factor": 2.25, + "trending_score": -0.011101, + "name": "VOC" + }, + { + "enable_display": true, + "sta": 671.416944, + "trending_score_validNumber": true, + "component_score": "Good", + "value": 671, + "id": 2, + "lta": 656.788879, + "lta_validNumber": true, + "sta_validNumber": true, + "scaling_factor": 1, + "trending_score": -0.001181, + "name": "CO2" + } + ], + "cleanliness_level_selected": "Basic", + "sz_sensor": 3, + "publisher": { + "writeAccess": "openAll", + "publisherName": "unknown", + "doNotPersist": true + } + } + } +} diff --git a/tests/test_sensor_iaq.py b/tests/test_sensor_iaq.py new file mode 100644 index 0000000..84d101e --- /dev/null +++ b/tests/test_sensor_iaq.py @@ -0,0 +1,105 @@ +"""Test BLE Sensors""" +# pylint: disable=line-too-long +import logging +from unittest.mock import patch +import pytest + +from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass + +from lennoxs30api.s30api_async import lennox_system +from custom_components.lennoxs30 import Manager +from custom_components.lennoxs30.const import LENNOX_DOMAIN + +from custom_components.lennoxs30.sensor import S40IAQSensor, lennox_iaq_sensors +from tests.conftest import conftest_base_entity_availability + + +@pytest.mark.asyncio +async def test_iaq_sensor(hass, manager_system_04_furn_ac_zoning_ble: Manager, caplog): + """Test the alert sensor""" + manager = manager_system_04_furn_ac_zoning_ble + system: lennox_system = manager.api.system_list[0] + ble_device = system.ble_devices[576] + sensor_dict = lennox_iaq_sensors[4] + sensor = S40IAQSensor(hass, manager, system, ble_device, sensor_dict) + + assert sensor.unique_id == (system.unique_id + "_BLE_576_iaq_pm25_lta").replace("-", "") + assert sensor.name == system.name + " " + ble_device.deviceName + " " + sensor_dict["name"] + assert sensor.available is True + assert sensor.should_poll is False + assert sensor.available is True + assert sensor.update() is True + assert sensor.state_class == SensorStateClass.MEASUREMENT + assert sensor.device_class == SensorDeviceClass.PM25 + assert sensor.extra_state_attributes is None + assert sensor.native_value == round(system.iaq_pm25_lta, sensor_dict["precision"]) + assert sensor.entity_category is None + assert sensor.native_unit_of_measurement is None + + system.iaq_pm25_lta_valid = False + assert sensor.available is False + system.iaq_pm25_lta_valid = True + assert sensor.available is True + + identifiers = sensor.device_info["identifiers"] + for ids in identifiers: + assert ids[0] == LENNOX_DOMAIN + assert ids[1] == system.unique_id + "_ble_576" + + with caplog.at_level(logging.WARNING): + caplog.clear() + system.iaq_pm25_lta = "NOT_A_NUMBER" + assert sensor.native_value is None + assert len(caplog.messages) == 1 + assert sensor.name in caplog.messages[0] + assert "NOT_A_NUMBER" in caplog.messages[0] + assert "could not convert" in caplog.messages[0] + + sensor_dict = lennox_iaq_sensors[0] + sensor = S40IAQSensor(hass, manager, system, ble_device, sensor_dict) + assert sensor.native_value == system.iaq_mitigation_action + + +@pytest.mark.asyncio +async def test_iaq_subscription(hass, manager_system_04_furn_ac_zoning_ble: Manager, caplog): + """Test the alert sensor subscription""" + manager = manager_system_04_furn_ac_zoning_ble + system: lennox_system = manager.api.system_list[0] + ble_device = system.ble_devices[576] + sensor_dict = lennox_iaq_sensors[4] + sensor = S40IAQSensor(hass, manager, system, ble_device, sensor_dict) + + await sensor.async_added_to_hass() + + with caplog.at_level(logging.DEBUG): + with patch.object(sensor, "schedule_update_ha_state") as update_callback: + caplog.clear() + update = {"iaq_pm25_lta": 0.1234} + system.attr_updater(update, "iaq_pm25_lta") + system.executeOnUpdateCallbacks() + assert update_callback.call_count == 1 + assert sensor.native_value == 0.1234 + assert len(caplog.messages) == 2 + assert sensor.name in caplog.messages[1] + assert "sensor_value_update" in caplog.messages[1] + + with patch.object(sensor, "schedule_update_ha_state") as update_callback: + caplog.clear() + update = {"iaq_pm25_lta_valid": False} + system.attr_updater(update, "iaq_pm25_lta_valid") + system.executeOnUpdateCallbacks() + assert update_callback.call_count == 1 + assert sensor.available is False + assert len(caplog.messages) == 2 + assert sensor.name in caplog.messages[1] + assert "sensor_value_update" in caplog.messages[1] + + with patch.object(sensor, "schedule_update_ha_state") as update_callback: + caplog.clear() + update = {"iaq_pm25_lta_valid": True} + system.attr_updater(update, "iaq_pm25_lta_valid") + system.executeOnUpdateCallbacks() + assert update_callback.call_count == 1 + assert sensor.available is True + + conftest_base_entity_availability(manager, system, sensor) diff --git a/tests/test_sensor_setup.py b/tests/test_sensor_setup.py index 6bd5a2b..0f5156e 100644 --- a/tests/test_sensor_setup.py +++ b/tests/test_sensor_setup.py @@ -26,6 +26,7 @@ S30OutdoorTempSensor, ) from custom_components.lennoxs30.sensor_ble import S40BleSensor +from custom_components.lennoxs30.sensor_iaq import S40IAQSensor from tests.conftest import loadfile @@ -248,9 +249,9 @@ async def test_async_setup_entry(hass, manager: Manager, caplog): await async_setup_entry(hass, entry, async_add_entities) assert async_add_entities.called == 1 sensor_list = async_add_entities.call_args[0][0] - assert len(sensor_list) == 22 - for index in range(0, 16): - assert isinstance(sensor_list[index], S40BleSensor) + assert len(sensor_list) == 34 + for index in range(0, 34): + assert isinstance(sensor_list[index], S40BleSensor | S40IAQSensor) with caplog.at_level(logging.ERROR): caplog.clear() @@ -260,7 +261,7 @@ async def test_async_setup_entry(hass, manager: Manager, caplog): await async_setup_entry(hass, entry, async_add_entities) assert async_add_entities.called == 1 sensor_list = async_add_entities.call_args[0][0] - assert len(sensor_list) == 20 + assert len(sensor_list) == 32 assert len(caplog.records) == 2 assert system.ble_devices[512].deviceName in caplog.messages[0] From 4aa8791c20ce34dc1d7434fec9fc41c0d6d28af8 Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Mon, 8 May 2023 16:38:47 -0400 Subject: [PATCH 5/9] Remove coverage files --- .coverage | Bin 69632 -> 0 bytes coverage.xml | 11477 ------------------------------------------------- 2 files changed, 11477 deletions(-) delete mode 100644 .coverage delete mode 100644 coverage.xml diff --git a/.coverage b/.coverage deleted file mode 100644 index f17ea83428f7f708ec4c577b8041780dd003c43a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69632 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 e_7(6G2Fa|eM - - - - - /mnt/c/github/lennoxs30 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From cbee32bd48b2a1ccff35b6adac229961db6c731e Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Mon, 8 May 2023 16:42:06 -0400 Subject: [PATCH 6/9] Delete .coverage --- .coverage | Bin 69632 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index 42dcd85bbe4361f91937eb4e2612f03c8dc6f04b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69632 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 Date: Mon, 8 May 2023 16:42:21 -0400 Subject: [PATCH 7/9] Delete coverage.xml --- coverage.xml | 11408 ------------------------------------------------- 1 file changed, 11408 deletions(-) delete mode 100644 coverage.xml diff --git a/coverage.xml b/coverage.xml deleted file mode 100644 index 1f7429b..0000000 --- a/coverage.xml +++ /dev/null @@ -1,11408 +0,0 @@ - - - - - - /mnt/c/github/lennoxs30 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 8a0072ce1507d635bf699de0951d2caf59293814 Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Mon, 8 May 2023 17:36:26 -0400 Subject: [PATCH 8/9] System tests 1. Add correct units 2. Update names and precisions for IAQ sensors 3. Bump versions --- .../lennoxs30/ble_device_21p02.py | 44 ++++++++++++------ custom_components/lennoxs30/manifest.json | 4 +- doc_images/iaq.PNG | Bin 0 -> 73480 bytes tests/test_sensor_iaq.py | 3 +- 4 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 doc_images/iaq.PNG diff --git a/custom_components/lennoxs30/ble_device_21p02.py b/custom_components/lennoxs30/ble_device_21p02.py index fc3bd96..3a5e505 100644 --- a/custom_components/lennoxs30/ble_device_21p02.py +++ b/custom_components/lennoxs30/ble_device_21p02.py @@ -2,7 +2,11 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.const import ( + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, +) lennox_21p02_sensors = [ { @@ -34,6 +38,7 @@ "name": "pm25", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.PM25, + "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, }, { "input_id": 4103, @@ -41,6 +46,7 @@ "name": "co2", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.CO2, + "uom": CONCENTRATION_PARTS_PER_MILLION, }, { "input_id": 4105, @@ -48,6 +54,8 @@ "name": "voc", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "precision": 2, }, ] @@ -60,74 +68,80 @@ lennox_iaq_sensors = [ { "input": "iaq_mitigation_action", - "name": "iaq mitigation action", + "name": "mitigation action", }, { "input": "iaq_mitigation_state", - "name": "iaq mitigation state", + "name": "mitigation state", }, { "input": "iaq_overall_index", - "name": "iaq overall index", + "name": "overall index", }, { "input": "iaq_pm25_sta", "status": "iaq_pm25_sta_valid", - "name": "iaq pm25 sta", + "name": "pm25 sta", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.PM25, + "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "precision": 4, }, { "input": "iaq_pm25_lta", "status": "iaq_pm25_lta_valid", - "name": "iaq pm25 lta", + "name": "pm25 lta", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.PM25, + "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "precision": 4, }, { "input": "iaq_pm25_component_score", - "name": "iaq pm25 component score", + "name": "pm25 component score", }, { "input": "iaq_voc_sta", "status": "iaq_voc_sta_valid", - "name": "iaq voc sta", + "name": "voc sta", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - "precision": 4, + "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "precision": 2, }, { "input": "iaq_voc_lta", "status": "iaq_voc_lta_valid", - "name": "iaq voc lta", + "name": "voc lta", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - "precision": 4, + "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "precision": 2, }, { "input": "iaq_voc_component_score", - "name": "iaq voc component score", + "name": "voc component score", }, { "input": "iaq_co2_lta", "status": "iaq_co2_lta_valid", - "name": "iaq co2 lta", + "name": "co2 lta", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.CO2, + "uom": CONCENTRATION_PARTS_PER_MILLION, "precision": 1, }, { "input": "iaq_co2_sta", "status": "iaq_co2_sta_valid", - "name": "iaq co2 sta", + "name": "co2 sta", "state_class": SensorStateClass.MEASUREMENT, "device_class": SensorDeviceClass.CO2, + "uom": CONCENTRATION_PARTS_PER_MILLION, "precision": 1, }, { "input": "iaq_co2_component_score", - "name": "iaq co2 component score", + "name": "co2 component score", }, ] diff --git a/custom_components/lennoxs30/manifest.json b/custom_components/lennoxs30/manifest.json index 1b2f8a3..801be48 100644 --- a/custom_components/lennoxs30/manifest.json +++ b/custom_components/lennoxs30/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "issue_tracker" : "https://github.com/PeteRager/lennoxs30/issues", "quality_scale": "platinum", - "requirements": ["lennoxs30api==0.2.4"], - "version": "2023.5.0" + "requirements": ["lennoxs30api==0.2.5"], + "version": "2023.5.1" } \ No newline at end of file diff --git a/doc_images/iaq.PNG b/doc_images/iaq.PNG new file mode 100644 index 0000000000000000000000000000000000000000..e25357ed9c28d8f1186ebd99e49a4f8894e3c0e4 GIT binary patch literal 73480 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

ChQ>e6k)0)#$GR6lXRk>_Jj-C_P{b|V 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 S2PVma3A5LAR12=c0{<7Tn3Mzn literal 0 HcmV?d00001 diff --git a/tests/test_sensor_iaq.py b/tests/test_sensor_iaq.py index 84d101e..ce48354 100644 --- a/tests/test_sensor_iaq.py +++ b/tests/test_sensor_iaq.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass +from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER from lennoxs30api.s30api_async import lennox_system from custom_components.lennoxs30 import Manager @@ -34,7 +35,7 @@ async def test_iaq_sensor(hass, manager_system_04_furn_ac_zoning_ble: Manager, c assert sensor.extra_state_attributes is None assert sensor.native_value == round(system.iaq_pm25_lta, sensor_dict["precision"]) assert sensor.entity_category is None - assert sensor.native_unit_of_measurement is None + assert sensor.native_unit_of_measurement == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER system.iaq_pm25_lta_valid = False assert sensor.available is False From 2aa2e23aeef5ae630c5636c3a2c982c4e3180ef8 Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Wed, 10 May 2023 07:58:43 -0400 Subject: [PATCH 9/9] Update readme, - are allowed in device ids --- README.MD | 38 ++++++++++++++++++++++++- custom_components/lennoxs30/__init__.py | 4 +-- tests/test_manager.py | 5 ++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/README.MD b/README.MD index 25de0e2..dd19b43 100644 --- a/README.MD +++ b/README.MD @@ -3,7 +3,6 @@ A custom component for Home Assistant to integrate with Lennox iComfort S40, S30, E30 or M30 thermostats; supporting local LAN connections and Lennox Cloud Connections depending on the device model. We believe these configurations work - let us know if your experience is different! - | Device Type | Local Connection | Cloud Connection | | ----------- | ---------------- | ---------------- | | S30 | Yes | Yes | @@ -320,6 +319,43 @@ The 22V25 is a battery powered room sensor. | binary_sensor | Device State | Unknown - need info | | binary_sensor | Occupancy | Indicates if the room is occupied | +### 21P02 - BLE Indoor Air Quality + +The 21P02 is a line powered air quality sensor. + +![plot](./doc_images/iaq.PNG) + +#### Sensors + +| Entity Type | Name | Units | Notes | +| ----------- | -------------------- | ------- | ------------------------------------------------ | +| sensor | Co2 | PPM | CO2 level | +| sensor | Co2 component score | Text | Fair, Good ? | +| sensor | Co2 lta | PPM | long term average | +| sensor | Co2 sta | PPM | short term average | +| sensor | Mitigation Action | Text | Current action being taken to addess air quality | +| sensor | Mitigation State | Text | ? | +| sensor | Overall Index | Text | Overall air quality - Fair, Good, ? | +| sensor | Pm25 | ug/m3 ? | Particulate Matter level | +| sensor | Pm25 component score | Text | Fair, Good ? | +| sensor | Pm25 lta | ug/m3 ? | long term average | +| sensor | Pm25 sta | ug/m3 ? | short term average | +| sensor | VOC | ug/m3 ? | Volatile Organic Compounds | +| sensor | VOC component score | Text | Fair, Good ? | +| sensor | VOC lta | ug/m3 ? | long term average | +| sensor | VOC sta | ug/m3 ? | short term average | + +#### Diagnostic Sensors + +| Entity Type | Name | Description | +| ------------- | ------------------ | ------------------------------------------------- | +| binary_sensor | Alarm Status | Unknown - need info | +| sensor | Ble rssi | Signal Strength. Unclear what this is vs rssi | +| binary_sensor | Comm_status | Indicates if communication to the device is up | +| binary_sensor | Device State | Unknown - need info | +| sensor | Rssi | Signal Strength. Unclear what this is vs ble_rssi | +| sensor | Total Powered Time | Time in seconds the device has been powered | + ## Sensors ### Zone Temperature and Humidity diff --git a/custom_components/lennoxs30/__init__.py b/custom_components/lennoxs30/__init__.py index f999c17..53c2269 100644 --- a/custom_components/lennoxs30/__init__.py +++ b/custom_components/lennoxs30/__init__.py @@ -610,7 +610,7 @@ async def _update_device_unique_ids(self, system: lennox_system): unique_id = x[1] if unique_id.startswith("123_"): suffix = unique_id.removeprefix("123_") - new_unique_id = f"{system.unique_id}_{suffix}".replace("-", "") + new_unique_id = f"{system.unique_id}_{suffix}" device_update_list[regentry.id] = new_unique_id _LOGGER.info( "Updating device [%s] identifier [%s] new unique id [%s]", @@ -619,7 +619,7 @@ async def _update_device_unique_ids(self, system: lennox_system): new_unique_id, ) elif unique_id == "123": - new_unique_id = system.unique_id.replace("-", "") + new_unique_id = system.unique_id device_update_list[regentry.id] = new_unique_id _LOGGER.info( "Updating device [%s] identifier [%s] new unique id [%s]", diff --git a/tests/test_manager.py b/tests/test_manager.py index cfe7d10..22336f8 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1025,12 +1025,11 @@ async def test_manager_unique_id_update(hass, manager_us_customary_units: Manage unique_id = None for unique_id in entry.identifiers: break - assert unique_id[1] == system.unique_id.replace("-", "") - + assert unique_id[1] == system.unique_id entry = dev_reg.async_get(id2) for unique_id in entry.identifiers: break - assert unique_id[1] == f"{system.unique_id}_iu".replace("-", "") + assert unique_id[1] == f"{system.unique_id}_iu" entry = dev_reg.async_get(id3) for unique_id in entry.identifiers: