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