diff --git a/.coverage b/.coverage
deleted file mode 100644
index c80d3d7..0000000
Binary files a/.coverage and /dev/null differ
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/coverage.xml b/coverage.xml
deleted file mode 100644
index 3d7b481..0000000
--- a/coverage.xml
+++ /dev/null
@@ -1,11260 +0,0 @@
-
-
-
-
-
- /mnt/c/github/lennoxs30
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/custom_components/lennoxs30/__init__.py b/custom_components/lennoxs30/__init__.py
index 5de84ee..53c2269 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}"
+ 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
+ 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/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..3a5e505
--- /dev/null
+++ b/custom_components/lennoxs30/ble_device_21p02.py
@@ -0,0 +1,147 @@
+"""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_MICROGRAMS_PER_CUBIC_METER,
+ 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": "pm25",
+ "state_class": SensorStateClass.MEASUREMENT,
+ "device_class": SensorDeviceClass.PM25,
+ "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ },
+ {
+ "input_id": 4103,
+ "status_id": 4104,
+ "name": "co2",
+ "state_class": SensorStateClass.MEASUREMENT,
+ "device_class": SensorDeviceClass.CO2,
+ "uom": CONCENTRATION_PARTS_PER_MILLION,
+ },
+ {
+ "input_id": 4105,
+ "status_id": 4106,
+ "name": "voc",
+ "state_class": SensorStateClass.MEASUREMENT,
+ "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
+ "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ "precision": 2,
+ },
+]
+
+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},
+]
+
+lennox_iaq_sensors = [
+ {
+ "input": "iaq_mitigation_action",
+ "name": "mitigation action",
+ },
+ {
+ "input": "iaq_mitigation_state",
+ "name": "mitigation state",
+ },
+ {
+ "input": "iaq_overall_index",
+ "name": "overall index",
+ },
+ {
+ "input": "iaq_pm25_sta",
+ "status": "iaq_pm25_sta_valid",
+ "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": "pm25 lta",
+ "state_class": SensorStateClass.MEASUREMENT,
+ "device_class": SensorDeviceClass.PM25,
+ "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ "precision": 4,
+ },
+ {
+ "input": "iaq_pm25_component_score",
+ "name": "pm25 component score",
+ },
+ {
+ "input": "iaq_voc_sta",
+ "status": "iaq_voc_sta_valid",
+ "name": "voc sta",
+ "state_class": SensorStateClass.MEASUREMENT,
+ "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
+ "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ "precision": 2,
+ },
+ {
+ "input": "iaq_voc_lta",
+ "status": "iaq_voc_lta_valid",
+ "name": "voc lta",
+ "state_class": SensorStateClass.MEASUREMENT,
+ "device_class": SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
+ "uom": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ "precision": 2,
+ },
+ {
+ "input": "iaq_voc_component_score",
+ "name": "voc component score",
+ },
+ {
+ "input": "iaq_co2_lta",
+ "status": "iaq_co2_lta_valid",
+ "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": "co2 sta",
+ "state_class": SensorStateClass.MEASUREMENT,
+ "device_class": SensorDeviceClass.CO2,
+ "uom": CONCENTRATION_PARTS_PER_MILLION,
+ "precision": 1,
+ },
+ {
+ "input": "iaq_co2_component_score",
+ "name": "co2 component score",
+ },
+]
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/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..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.3"],
- "version": "2023.5.0"
+ "requirements": ["lennoxs30api==0.2.5"],
+ "version": "2023.5.1"
}
\ No newline at end of file
diff --git a/custom_components/lennoxs30/sensor.py b/custom_components/lennoxs30/sensor.py
index e18721d..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,9 +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, lennox_iaq_sensors
from .sensor_ble import S40BleSensor
+from .sensor_iaq import S40IAQSensor
-from . import Manager
_LOGGER = logging.getLogger(__name__)
@@ -119,8 +120,15 @@ 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":
+ 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:
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/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/doc_images/iaq.PNG b/doc_images/iaq.PNG
new file mode 100644
index 0000000..e25357e
Binary files /dev/null and b/doc_images/iaq.PNG differ
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/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"
+ }
+ }
+ }
+ }
+]
+}
diff --git a/tests/messages/system_04_furn_ac_zoning_ble.json b/tests/messages/system_04_furn_ac_zoning_ble.json
index d25db46..5c3aea8 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": "1234567"
+ }
+ ],
+ "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/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_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_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_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_manager.py b/tests/test_manager.py
index 2899188..22336f8 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,125 @@ 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
+ entry = dev_reg.async_get(id2)
+ for unique_id in entry.identifiers:
+ break
+ assert unique_id[1] == f"{system.unique_id}_iu"
+
+ 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):
diff --git a/tests/test_sensor_iaq.py b/tests/test_sensor_iaq.py
new file mode 100644
index 0000000..ce48354
--- /dev/null
+++ b/tests/test_sensor_iaq.py
@@ -0,0 +1,106 @@
+"""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 homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+
+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 == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
+
+ 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 ec1e90a..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) == 16
- 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) == 14
+ assert len(sensor_list) == 32
assert len(caplog.records) == 2
assert system.ble_devices[512].deviceName in caplog.messages[0]
@@ -277,6 +278,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