Compare commits

...

20 Commits

Author SHA1 Message Date
a03e12dfa7 Readd health scores 2025-06-29 13:04:46 -04:00
1f7df559b1 Only fire light_off on occupancy change
Avoids constantly turning off a held LED.
2025-06-23 23:38:11 -04:00
d75a7d26a6 Increase MWW gain factor to 8
Improves reliability.
2025-06-23 23:19:30 -04:00
e4c1ab1ac2 Fix typos and bugs in config 2025-06-22 00:45:41 -04:00
efd9fc8bfe Update designs for SGPXX change 2025-06-22 00:09:20 -04:00
9aebae647a Update README parts list and descriptions 2025-06-22 00:08:10 -04:00
3300ca2d8e Switch to SGP41 air quality sensor
My testing with the SGP30 sensor proved fruitless, as various sensors
would mis-report (under- and over-) levels and generally not be
consistent with each other or reality. These sensors are simply too
flaky with zero consistency.

Instead, switch to the SGP41, which so far seems more robust and is used
in other tools like the AirGradient One. We leverage their calculations
for VOC Index -> VOC levels, as well as a generalized isobutylene-based
eCO2 calculation.
2025-06-21 23:48:42 -04:00
0ba3b855d0 Add warning 2025-06-15 12:51:59 -04:00
4f6597d92f Switch logging to DEBUG during testing 2025-06-15 12:51:59 -04:00
985e7e20e5 Add two more icons 2025-06-08 21:04:45 -04:00
426684e9da Update icon of VOC 2025-06-07 19:03:36 -04:00
7fe3829544 Restore debug components for 2025.5.2
Now that the debug: components will work without debug-level logging,
re-enable the useful ones.
2025-06-03 10:21:33 -04:00
206691257d Add Computer wakeword and selector 2025-05-30 23:37:56 -04:00
aca7e16ed0 Add regular MWW state check
Ensures that if the micro_wake_word component stops, it will be
automatically restarted on a regular interval.
2025-05-28 23:05:53 -04:00
556f8564c4 Add ESPHome version sensor 2025-05-27 22:00:06 -04:00
8114d765f0 Adjust ideal humidity levels
I don't want to detract with 30-40 or 60-70% humidity.
2025-05-26 11:57:51 -04:00
d529f695a3 Adjust levels further 2025-05-25 02:18:29 -04:00
c0b5adecca Adjust gain levels 2025-05-25 02:14:21 -04:00
5c94e56847 Add icons to various sensors 2025-05-24 21:19:10 -04:00
fd7e438c62 Adjust AQ wordings 2025-05-24 20:00:30 -04:00
4 changed files with 845 additions and 734 deletions

168
README.md
View File

@ -1,5 +1,8 @@
# SuperSensor v2.x # SuperSensor v2.x
**NOTICE**: The Supersensor v2.x is still under development! Parts and configurations
may change until the design is finalized.
The SuperSensor is an all-in-one voice, motion, presence, temperature/humidity/air The SuperSensor is an all-in-one voice, motion, presence, temperature/humidity/air
quality, and light sensor, built on an ESP32 with ESPHome, and inspired quality, and light sensor, built on an ESP32 with ESPHome, and inspired
heavily by the EverythingSmartHome Everything Presence One sensor and the heavily by the EverythingSmartHome Everything Presence One sensor and the
@ -18,8 +21,7 @@ it bare if you like the "PCB on a wall" aesthetic.
To Use: To Use:
* Install the ESPHome configuration `supersensor.yaml` to a compatible ESP32 devkit (below). * Install the ESPHome configuration `supersensor.yaml` to a compatible ESP32 devkit (below).
* Install the ESP32 and sensors into the custom PCB (if desired). * Install the ESP32 and sensors into the custom PCB.
* [Optional] 3D Print the custom case.
* Power up the SuperSensor, connect to the WiFi AP, and connect it to your network. * Power up the SuperSensor, connect to the WiFi AP, and connect it to your network.
* Install the SuperSensor somewhere that makes sense. * Install the SuperSensor somewhere that makes sense.
* Add/adopt the SuperSensor to HomeAssistant using the automatic name. * Add/adopt the SuperSensor to HomeAssistant using the automatic name.
@ -39,15 +41,15 @@ and [my update post on version 2.0](https://www.boniface.me/the-supersensor-2.0)
## Major Changes from 1.x ## Major Changes from 1.x
1. Replaced the Bosch BME680 with the Sensirion SHT45 and Sensirion SGP30. 1. Replaced the Bosch BME680 with the Sensirion SHT45 and Sensirion SGP41.
The BME680 proved to be woefully unreliable in my testing. Temperature was fairly accurate (internal heating and offset notwithstanding), The BME680 proved to be woefully unreliable in my testing. Temperature was fairly accurate (internal heating and offset notwithstanding),
but humidity was wildly off of what other thermometers/hydrometers would report. In addition, the AQ functionality of the sensor was a but humidity was wildly off of what other thermometers/hydrometers would report. In addition, the AQ functionality of the sensor was a
source of much frustration and I was never able to get it to work reliably, either with the official BSEC library or with my own attempts source of much frustration and I was never able to get it to work reliably, either with the official BSEC library or with my own attempts
at self-configuration. at self-configuration.
Thus, this sensor has been replaced with two Sensirion sensors which in my experience so far have been much more reliable and consistent, Thus, this sensor has been replaced with two Sensirion sensors which in my experience so far have been much more reliable and consistent.
and the cost difference is negligible. There is a slight cost increase due to these sensors, but not signfigant enough to outweigh the benefit of reliable monitoring they confer.
2. Replaced the SR602 PIR sensor with the AM312 PIR sensor. 2. Replaced the SR602 PIR sensor with the AM312 PIR sensor.
@ -61,15 +63,12 @@ and [my update post on version 2.0](https://www.boniface.me/the-supersensor-2.0)
3. Completely redesigned the custom PCB around the above sensor changes, which is now more compact in a 50x55mm almost-square configuration. 3. Completely redesigned the custom PCB around the above sensor changes, which is now more compact in a 50x55mm almost-square configuration.
4. Significantly cleaned up the ESPHome configuration, to support the above sensors and remove a lot of cruft that was caused by the BME680. 4. Significantly cleaned up the ESPHome configuration, to support the above sensors and remove a lot of cruft that was caused by the BME680.
This includes a new set of custom AQ calculations based on the SGP30 and SHT45 sensors that, while not necessarily following the full EPA
IAQI spec, should still give a reasonable view of the air quality conditions of an interior room and not deviate wildly and nonsensically
like the BME680 did. Details of the calculation are provided below.
## Parts List ## Parts List
| Qty | Component | Cost (2025/05 CAD, ex. shipping) | Links | | Qty | Component | Cost (2025/05 CAD, ex. shipping) | Links |
|-------|--------------------|----------------------------------|-------| |-------|--------------------|----------------------------------|-------|
| 1 | GY-SGP30 | $5.73 | [AliExpress](https://www.aliexpress.com/item/1005008473372972.html) | | 1 | GY-SGP41 | $11.08 | [AliExpress](https://www.aliexpress.com/item/1005006746827606.html) |
| 1 | GY-SHT45 | $5.67 | [AliExpress](https://www.aliexpress.com/item/1005008175340220.html)* | | 1 | GY-SHT45 | $5.67 | [AliExpress](https://www.aliexpress.com/item/1005008175340220.html)* |
| 1 | SR602 | $0.81 | [AliExpress](https://www.aliexpress.com/item/1005001572550300.html) | | 1 | SR602 | $0.81 | [AliExpress](https://www.aliexpress.com/item/1005001572550300.html) |
| 1 | TSL2591 | $4.59 | [AliExpress](https://www.aliexpress.com/item/1005008619462097.html) | | 1 | TSL2591 | $4.59 | [AliExpress](https://www.aliexpress.com/item/1005008619462097.html) |
@ -78,16 +77,59 @@ and [my update post on version 2.0](https://www.boniface.me/the-supersensor-2.0)
| 1 | ESP32 HW-395 | $6.67 | [AliExpress](https://www.aliexpress.com/item/1005006019875837.html)* | | 1 | ESP32 HW-395 | $6.67 | [AliExpress](https://www.aliexpress.com/item/1005006019875837.html)* |
| 2 | RBG LED | $0.09 ($9.12/100) | [Amazon](https://www.amazon.ca/dp/B09Y8M2PKS) | | 2 | RBG LED | $0.09 ($9.12/100) | [Amazon](https://www.amazon.ca/dp/B09Y8M2PKS) |
| 1 | 470Ω resistor | $0.08 ($7.99/100) | [Amazon](https://www.amazon.ca/dp/B08MKQX2XT) | | 1 | 470Ω resistor | $0.08 ($7.99/100) | [Amazon](https://www.amazon.ca/dp/B08MKQX2XT) |
| 1 | Female pin header† | $1.59 ($15.99/10) | [Amazon](https://www.amazon.ca/dp/B08CMNRXJ1) | | 2 | Female pin header† | $1.59 ($15.99/10) | [Amazon](https://www.amazon.ca/dp/B08CMNRXJ1) |
| 1 | Custom PCB (JLC) | $0.69 ($6.89/10) | [GitHub](https://github.com/joshuaboniface/supersensor) | | 1 | Custom PCB (JLC) | $0.69 ($6.89/10) | [GitHub](https://github.com/joshuaboniface/supersensor) |
| 1 | 3D Printed case | $?.??‡ | [GitHub](https://github.com/joshuaboniface/supersensor) | | **TOTAL** | | **$40.58** | |
| **TOTAL** | | **$33.64** | |
`*` Ensure you select the correct device on the page as it shows multiple options. `*` Ensure you select the correct device on the page as it shows multiple options.
`†` This is optional and only required if you don't want to directly solder the ESP32 to the board, but I recommend it. `†` One of these sets is optional, and is useful if you do not want to solder the individual sensors directly to the board (see below).
`‡` Providing a price is impossible due to the wide range of possible fillament types and brands, but should be negligible. ### To Solder or Not To Solder
Personally, for my Supersensor 1.x's and the initial batch of Supersensor 2.x's, I directly soldered
all the non-ESP components to the board. This proved to be a major mistake when I later decided
to switch from SGP30's to SGP41's after some testing and I had to desolder all of them, ruining
several PCBs in the process. It was also a hassle to desolder the existing sensors for reuse
during the 1.x to 2.x conversion.
As a result, I actually strongly encourage anyone building one of these units to leverage sockets
for all components, to allow for quick swapping if any turn out to be defective or if future changes
are warranted.
Note that due to the PCB design, you *must* socket at least one set of components - either the ESP32
or the sensors on the front. Due to the positioning and overlap, it would be impossible to solder
everything directly to the board, as the ESP covers several of the solder points of the front
sensors and vice versa.
You can use the provided 40-pin female headers exclusively if you wish, and cut them to length for
the individual sensors as needed, or you can use individually-sized female headers in the following
quantities should you wish for a slightly neater finish:
* 3x 3-pin (AM302, INMP441 x2)
* 2x 4-pin (SGP41, SHT45)
* 1x 5-pin (LD2410C)
* 1x 6-pin (TSL2591)
I will leave it up to the reader to source these specific sizes if they desire (I found all except
a 5-pin on Amazon, and just used a 6-pin with one pin removed).
I still directly solder the RGB LEDs and resistor to the board for simplicity as these very small
leads are not easily socketed, and these components are so inexpensive as to be effectively
disposable along with the PCB should that be required.
### Part Swaps
To save a little money, it is possible to swap out the two Sensirion sensors for their less-feature-
rich peers, with no code changes:
* SGP41 -> SGP40 - removes the NOx functionality
* SHT45 -> SHT40/41/43 - less accuracy
Personally, I do not find the minimal cost savings to be worth sacrificing the extra potential
functionality, so I recommend using the provided models, but this is up to the builder to decide.
No other parts can be easily swapped without code or PCB design changes.
## Configurable Options ## Configurable Options
@ -109,17 +151,19 @@ SuperSensors in a single room and only want one to respond to voice commands.
If enabled (the default), when overall presence is detected, the LEDs will If enabled (the default), when overall presence is detected, the LEDs will
glow "white" at 15% power to signal presence. glow "white" at 15% power to signal presence.
### Temperature Offset (selector, -10 to +5 @ 0.1, -5 default) ### Temperature Offset (selector, -30 to +10 @ 0.1, -5 default)
Allows calibration of the SHT45 temperature sensor with an offset from -10 to +5 Allows calibration of the SHT45 temperature sensor with an offset from -30 to +10
degrees C. Useful if the sensor is misreporting actual ambient tempreatures. Due degrees C. Useful if the sensor is misreporting actual ambient tempreatures. Due
to internal heating of the SHT45 by the ESP32, this defaults to -5; further to internal heating of the SHT45 by the ESP32, this defaults to -5; further
calibration may be needed for your sensors and environment. calibration may be needed for your sensors and environment based on an external
reference.
### Humidity Offset (selector, -10 to +10 @ 0.1) ### Humidity Offset (selector, -20 to +20 @ 0.1)
Allows calibration of the SHT45 humidity sensor with an offset from -10 to +10 Allows calibration of the SHT45 humidity sensor with an offset from -10 to +10
percent relative humidity. Useful if the sensor is misreporting actual humidity. percent relative humidity. Useful if the sensor is misreporting actual humidity
based on an external reference.
### PIR Hold Time (selector, 0 to +60 @ 5, 0 default) ### PIR Hold Time (selector, 0 to +60 @ 5, 0 default)
@ -227,70 +271,28 @@ is likely not useful.
## AQ Details ## AQ Details
The SuperSensor 2.x provides 2 base air quality sensors (numeric), from which The SuperSensor 2.0 features an SGP41 air quality sensor by Sensirion. This is a powerful AQ
4 human-readable text sensors are derived. sensor which powers several commercial devices including the AirGradient One, which gave
us a lot of our configuration via their sharing of algorithms.
The goal of these sensors is to track general comfort and livability in a The sensor provides two base readings: a VOC Index, and a NOx Index. These values are both
room, not specific contaminants or conditions. Because the SGP30 can only floating references centered at 100 (VOC) and 1 (NOx), where that value represents "normal"
track TVOC and eCO2, we do not track particulates, CO, NOx, or CH2O, all air over the previous 24 hours. These sensors are very useful for any sort of quick-change
of which are required for a full EPA (I)AQI score. This means the best automations, e.g. turn on a fan if levels spike due to cooking.
we can do is approximate (I)AQI roughly, and since a scale of 0-500 based
on approximations seems pointless, I went with much simpler 1-4/5 scores
instead. I feel this does a good enough job to be useful for 99% of rooms.
We also cannot really debate whether the BME680 is actually any more accurate In addition, we leverage AirGradient's published forumulas to convert the VOC index into
in this regard, since their algorithms are proprietary and all that is exposed actual VOC quantities, in both µg/m³ and ppb. While this may drift due to the sensor's regular
normally is a single resistance value, so in my opinion this is actually internal recalibration, I feel that following what AirGradient does is sufficient enough
superior to that sensor anyways with two discrete datapoints (versus one), for any real-world home usage. Further, we use a very rough conversion of the aforementioned
even if it does still seem limited when compared to dedicated AQ sensors. VOC quantity into an eCO2 reading, using Isobutylene as a reference gas. These sensors are
And that is to say nothing of the issues with that sensor (constantly climbing more useful for display purposes, to show the current levels in a room in a dashboard or
IAQ values over time, poor calibration, etc.). other such place, for human consumption. Note that no such conversions are done for NOx as
there are no (that I can find) published empirical calculations for this conversion, unlike
for VOCs via AirGradient.
### Base Numeric Values Note however that like all MOx sensors, the SGP41 does not differentiate gasses, and as
such cannot tell the difference between normal, everyday natural VOCs like those in
#### IAQ Index (1-5) breath or from e.g. ripening fruit, and dangerous VOCs from e.g. construction materials.
These should be used only as a general indication of air quality over short periods, rather
The IAQ index is calculated based on the TVOC and eCO2 values from the SGP30 than an absolute reference over long periods (much to my own frustration but inevitable
sensor, to provide 5 levels of air quality. This corresponds approximately begruding acceptance).
to the levels provided by the BME680 (0-50, 50-100, 100-200, 200-300, 300+).
5 is "excellent": the TVOC is <65 ppb and the eCO2 is <600 ppm.
4 is "good": the TVOC is 65-220 ppb or the eCO2 is 600-800 ppm.
3 is "moderate": the TVOC is 220-660 ppb or the eCO2 is 800-1200 ppm.
2 is "poor": the TVOC is 660-2200 ppb or the eCO2 is 1200-2000 ppm.
1 is "unhealthy": the TVOC is >2200 ppb or the eCO2 is >2000 ppm.
#### Room Health Score (1-4)
The Room Health Score is calculated based on the IAQ, temperature, and humidity,
and is designed to show how "nice" a room is to be in. Generally a 4 is a nice
place to be, especially for someone with respiratory issues like myself, and lower
scores indicate more deviations from the norms or poor IAQ.
4 is "optimal": IAQ is >= 4 ("excellent" or "good"), temperature is between 18C and 24C, and humidity is between 40% and 60%.
3 is "fair": One of the above is not true, and IAQ is >= 3 ("moderate").
2 is "poor": Two of the above are not true, and IAQ is >= 2 ("poor").
1 is "bad": All of the above are not true or IAQ is 1 ("unhealthy") regardless of other values.
Note that IAQ levels hold a major sway over this level, and decreasing IAQ
scores will push the room score lower regardless of temperature or humidity.
It is best used together with the individual sensors to determine exactly
what is wrong with the room.
### Derived Text Sensors
#### VOC Level
This reports the VOC level alone, based on the scale under IAQ Index, in textual form ("Excellent, "Good", etc.).
#### CO2 Level
This reports the eCO2 level alone, based on the scale under IAQ Index, in textual form ("Excellent, Good", etc.).
#### IAQ Classification
This reports the IAQ Index in textual form ("Excellent", "Good", etc.).
#### Room Health
This reports the Room Health Score in textual form ("Optimal", "Fair", "Poor", "Bad").

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -103,6 +103,11 @@ globals:
restore_value: no restore_value: no
initial_value: "0" initial_value: "0"
- id: current_wake_word
type: std::string
restore_value: yes
initial_value: '"mww_computer"'
script: script:
- id: light_off - id: light_off
then: then:
@ -253,6 +258,17 @@ script:
} }
interval: interval:
# Regular MWW state check every 30s
- interval: 30s
then:
- if:
condition:
- switch.is_on: enable_voice_support
- not:
micro_wake_word.is_running
then:
- micro_wake_word.start:
# Regular occupancy state reporting to HASS every 30s # Regular occupancy state reporting to HASS every 30s
- interval: 30s - interval: 30s
then: then:
@ -273,7 +289,7 @@ interval:
} }
logger: logger:
level: WARN level: DEBUG
baud_rate: 115200 baud_rate: 115200
api: api:
@ -330,6 +346,9 @@ time:
then: then:
- logger.log: "Time synchronized with Home Assistant" - logger.log: "Time synchronized with Home Assistant"
debug:
update_interval: 15s
uart: uart:
id: ld2410_uart id: ld2410_uart
rx_pin: GPIO19 rx_pin: GPIO19
@ -364,9 +383,11 @@ micro_wake_word:
id: mww id: mww
microphone: microphone:
microphone: mic microphone: mic
gain_factor: 4 gain_factor: 8
stop_after_detection: false stop_after_detection: false
models: models:
- model: github://genehand/Custom_V2_MicroWakeWords/models/computer/computer.json@update-json
id: mww_computer
- model: github://esphome/micro-wake-word-models/models/v2/hey_jarvis.json - model: github://esphome/micro-wake-word-models/models/v2/hey_jarvis.json
id: mww_hey_jarvis id: mww_hey_jarvis
- model: github://esphome/micro-wake-word-models/models/v2/hey_mycroft.json - model: github://esphome/micro-wake-word-models/models/v2/hey_mycroft.json
@ -392,8 +413,8 @@ voice_assistant:
micro_wake_word: mww micro_wake_word: mww
use_wake_word: false use_wake_word: false
noise_suppression_level: 3 noise_suppression_level: 3
auto_gain: 4 dbfs auto_gain: 31 dbfs
volume_multiplier: 8 volume_multiplier: 4
on_wake_word_detected: on_wake_word_detected:
- logger.log: "Wake word detected in VA pipeline" - logger.log: "Wake word detected in VA pipeline"
- light.turn_on: - light.turn_on:
@ -515,34 +536,75 @@ sensor:
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024; return heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024;
entity_category: diagnostic entity_category: diagnostic
- platform: sgp30 - platform: debug
eco2: free:
name: "SGP30 eCO2" name: "Heap Free"
id: sgp30_eco2 block:
accuracy_decimals: 1 name: "Heap Max Block"
loop_time:
name: "Loop Time"
cpu_frequency:
name: "CPU Frequency"
- platform: sgp4x
voc:
name: "SGP41 VOC Index"
id: sgp41_voc_index
accuracy_decimals: 0
icon: mdi:waves-arrow-up
filters:
- sliding_window_moving_average: # We take a reading every 15 seconds, but calculate the sliding
window_size: 12 # average over 12 readings i.e. 60 seconds/1 minute to normalize
send_every: 3 # brief spikes while still sending a value every 15 seconds.
nox:
name: "SGP41 NOx Index"
id: sgp41_nox_index
accuracy_decimals: 0
icon: mdi:waves-arrow-up
filters: filters:
- sliding_window_moving_average: - sliding_window_moving_average:
window_size: 20 window_size: 12
send_every: 1 send_every: 3
tvoc:
name: "SGP30 TVOC"
id: sgp30_tvoc
accuracy_decimals: 1
filters:
- sliding_window_moving_average:
window_size: 20
send_every: 1
eco2_baseline:
name: "SGP30 Baseline eCO2"
id: sgp30_baseline_ec02
tvoc_baseline:
name: "SGP30 Baseline TVOC"
id: sgp30_baseline_tvoc
compensation: compensation:
temperature_source: sht45_temperature temperature_source: sht45_temperature
humidity_source: sht45_humidity humidity_source: sht45_humidity
store_baseline: yes store_baseline: true
update_interval: 15s update_interval: 5s
- platform: template
name: "SGP41 TVOC (µg/m³)"
id: sgp41_tvoc_ugm3
icon: mdi:molecule
lambda: |-
float i = id(sgp41_voc_index).state;
if (i < 1) return NAN;
float tvoc = (log(501.0 - i) - 6.24) * -878.53;
return tvoc;
unit_of_measurement: "µg/m³"
accuracy_decimals: 0
- platform: template
name: "SGP41 TVOC (ppb)"
id: sgp41_tvoc_ppb
icon: mdi:molecule
lambda: |-
float tvoc_ugm3 = id(sgp41_tvoc_ugm3).state;
float tvoc_ppm = tvoc_ugm3 * 0.436; // ppb estimated using isobutylene MW (56.1 g/mol)
return tvoc_ppm;
unit_of_measurement: "ppb"
accuracy_decimals: 0
- platform: template
name: "SGP41 eCO2 (appr.)"
id: sgp41_eco2_appr
icon: mdi:molecule-co2
lambda: |-
float tvoc_ppb = id(sgp41_tvoc_ppb).state;
float eco2_ppm = 400.0 + 1.5 * tvoc_ppb;
if (eco2_ppm > 2000) eco2_ppm = 2000;
return eco2_ppm;
unit_of_measurement: "ppm"
accuracy_decimals: 0
- platform: sht4x - platform: sht4x
temperature: temperature:
@ -572,9 +634,9 @@ sensor:
humidity: sht45_humidity humidity: sht45_humidity
id: sht45_absolute_humidity id: sht45_absolute_humidity
# Dew Point
- platform: template - platform: template
name: "SHT45 Dew Point" name: "SHT45 Dew Point"
icon: mdi:thermometer-water
id: sht45_dew_point id: sht45_dew_point
unit_of_measurement: "°C" unit_of_measurement: "°C"
lambda: |- lambda: |-
@ -586,47 +648,38 @@ sensor:
return (b * alpha) / (a - alpha); return (b * alpha) / (a - alpha);
update_interval: 15s update_interval: 15s
# IAQ Index (1-5, 5=Excellent)
- platform: template
name: "IAQ Index"
id: iaq_index
lambda: |-
int tvoc = id(sgp30_tvoc).state;
int eco2 = id(sgp30_eco2).state;
if (tvoc > 2200 || eco2 > 2000) return 1; // Unhealthy
if (tvoc > 660 || eco2 > 1200) return 2; // Poor
if (tvoc > 220 || eco2 > 800) return 3; // Moderate
if (tvoc > 65 || eco2 > 500) return 4; // Good
return 5; // Excellent
update_interval: 15s
# Room Health Score (1-4, 4=Optimal)
- platform: template - platform: template
name: "Room Health Score" name: "Room Health Score"
id: room_health id: room_health_score
unit_of_measurement: "%"
accuracy_decimals: 0
icon: mdi:home-heart
lambda: |- lambda: |-
float voc_index = id(sgp41_voc_index).state;
float temp = id(sht45_temperature).state; float temp = id(sht45_temperature).state;
float rh = id(sht45_humidity).state; float humidity = id(sht45_humidity).state;
int iaq = id(iaq_index).state;
bool temp_ok = (temp >= 18 && temp <= 24); // VOC Score (0100)
bool hum_ok = (rh >= 40 && rh <= 60); float voc_score = 0;
bool iaq_ok = (iaq >= 4); if (voc_index <= 100) voc_score = 100;
else if (voc_index <= 200) voc_score = 80;
else if (voc_index <= 300) voc_score = 60;
else if (voc_index <= 400) voc_score = 40;
else if (voc_index <= 500) voc_score = 50;
else voc_score = 0;
int conditions_met = 0; // Temperature Score (0100)
if (temp_ok) conditions_met++; float temp_score = 100.0 - abs(temp - 23.0) * 10.0;
if (hum_ok) conditions_met++; if (temp_score < 0) temp_score = 0;
if (iaq_ok) conditions_met++;
if (iaq_ok && temp_ok && hum_ok) { // Humidity Score (0100), ideal range 3555%
return 4; // Optimal: All conditions met and IAQ is excellent/good float humidity_score = 100.0 - abs(humidity - 50.0) * 3.0;
} else if (iaq >= 3 && conditions_met >= 2) { if (humidity_score < 0) humidity_score = 0;
return 3; // Fair: IAQ is moderate and at least 2 conditions met
} else if (iaq >= 2 && conditions_met >= 1) { // Weighted average
return 2; // Poor: IAQ is poor and at least 1 condition met float overall_score = (voc_score * 0.5 + temp_score * 0.25 + humidity_score * 0.25);
} else {
return 1; // Bad: All conditions failed or IAQ is unhealthy return round(overall_score);
}
update_interval: 15s update_interval: 15s
- platform: tsl2591 - platform: tsl2591
@ -679,9 +732,8 @@ binary_sensor:
name: "SuperSensor Occupancy" name: "SuperSensor Occupancy"
id: supersensor_occupancy id: supersensor_occupancy
device_class: occupancy device_class: occupancy
on_press: on_state:
- script.execute: light_off then:
on_release:
- script.execute: light_off - script.execute: light_off
- platform: gpio - platform: gpio
@ -731,6 +783,10 @@ binary_sensor:
name: "LD2410C Still Target" name: "LD2410C Still Target"
text_sensor: text_sensor:
- platform: version
name: "ESPHome Version"
entity_category: diagnostic
- platform: wifi_info - platform: wifi_info
ip_address: ip_address:
name: "WiFi IP Address" name: "WiFi IP Address"
@ -741,57 +797,44 @@ text_sensor:
mac_address: mac_address:
name: "WiFi MAC Address" name: "WiFi MAC Address"
- platform: debug
device:
name: "Device Info"
reset_reason:
name: "Reset Reason"
- platform: ld2410 - platform: ld2410
version: version:
name: "LD2410C Firmware Version" name: "LD2410C Firmware Version"
mac_address: mac_address:
name: "LD2410C MAC Address" name: "LD2410C MAC Address"
# VOC Level
- platform: template - platform: template
name: "VOC Level" name: "Chemical Pollution"
id: sgp41_chemical_pollution
icon: mdi:molecule
lambda: |- lambda: |-
int tvoc = id(sgp30_tvoc).state; float voc_index = id(sgp41_voc_index).state;
if (tvoc < 65) return {"Excellent"}; if (voc_index < 1 || voc_index > 500) return {"Unknown"};
if (tvoc < 220) return {"Good"}; if (voc_index <= 100) return {"Excellent"};
if (tvoc < 660) return {"Moderate"}; else if (voc_index <= 200) return {"Good"};
if (tvoc < 2200) return {"Poor"}; else if (voc_index <= 300) return {"Moderate"};
return {"Unhealthy"}; else if (voc_index <= 400) return {"Unhealthy"};
else return {"Hazardous"};
update_interval: 15s update_interval: 15s
# CO2 Level
- platform: template
name: "CO2 Level"
lambda: |-
int eco2 = id(sgp30_eco2).state;
if (eco2 < 500) return {"Excellent"};
if (eco2 < 800) return {"Good"};
if (eco2 < 1200) return {"Moderate"};
if (eco2 < 2000) return {"Poor"};
return {"Unhealthy"};
update_interval: 15s
# IAQ Classification
- platform: template
name: "IAQ Classification"
lambda: |-
int iaq = id(iaq_index).state;
if (iaq == 5) return {"Excellent"};
if (iaq == 4) return {"Good"};
if (iaq == 3) return {"Moderate"};
if (iaq == 2) return {"Poor"};
return {"Unhealthy"};
update_interval: 15s
# Room Health
- platform: template - platform: template
name: "Room Health" name: "Room Health"
id: room_health_text
icon: mdi:home-heart
lambda: |- lambda: |-
int score = id(room_health).state; float score = id(room_health_score).state;
if (score == 4) return {"Optimal"}; if (score < 0) return {"Unknown"};
if (score == 3) return {"Fair"}; else if (score >= 90.0) return {"Great"};
if (score == 2) return {"Poor"}; else if (score >= 80.0) return {"Good"};
return {"Bad"}; else if (score >= 60.0) return {"Fair"};
else if (score >= 40.0) return {"Poor"};
else return {"Bad"};
update_interval: 15s update_interval: 15s
button: button:
@ -1098,34 +1141,100 @@ select:
name: "LD2410C Distance Resolution" name: "LD2410C Distance Resolution"
- platform: template - platform: template
name: "Wake word sensitivity" name: "Wake Word Selector"
optimistic: true id: wake_word_selector
initial_option: Moderately sensitive
restore_value: true
entity_category: config
options: options:
- Slightly sensitive - "Computer"
- Moderately sensitive - "Hey Jarvis"
- "Hey Mycroft"
- "Okay Nabu"
- "Alexa"
initial_option: "Computer"
optimistic: true
restore_value: true
set_action:
# Disable models that aren't selected
- if:
condition:
lambda: 'return x != "Computer";'
then:
- micro_wake_word.disable_model: mww_computer
- if:
condition:
lambda: 'return x != "Hey Jarvis";'
then:
- micro_wake_word.disable_model: mww_hey_jarvis
- if:
condition:
lambda: 'return x != "Hey Mycroft";'
then:
- micro_wake_word.disable_model: mww_hey_mycroft
- if:
condition:
lambda: 'return x != "Okay Nabu";'
then:
- micro_wake_word.disable_model: mww_okay_nabu
- if:
condition:
lambda: 'return x != "Alexa";'
then:
- micro_wake_word.disable_model: mww_alexa
# Enable model we selected
- if:
condition:
lambda: 'return x == "Computer";'
then:
- micro_wake_word.enable_model: mww_computer
- if:
condition:
lambda: 'return x == "Hey Jarvis";'
then:
- micro_wake_word.enable_model: mww_hey_jarvis
- if:
condition:
lambda: 'return x == "Hey Mycroft";'
then:
- micro_wake_word.enable_model: mww_hey_mycroft
- if:
condition:
lambda: 'return x == "Okay Nabu";'
then:
- micro_wake_word.enable_model: mww_okay_nabu
- if:
condition:
lambda: 'return x == "Alexa";'
then:
- micro_wake_word.enable_model: mww_alexa
- platform: template
name: "Wake Word Sensitivity"
optimistic: true
initial_option: Default
restore_value: true
options:
- Default
- More sensitive
- Very sensitive - Very sensitive
on_value: set_action:
# Sets specific wake word probabilities computed for each particular model # Sets specific wake word probabilities computed for each particular model
# Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff # Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff
# False Accepts per Hour values are tested against all units and channels from the Dinner Party Corpus. - lambda: |-
# These cutoffs apply only to the specific models included in the firmware: okay_nabu@20241226.3, hey_jarvis@v2, hey_mycroft@v2 if (x == "Default") {
lambda: |- id(mww_computer).set_probability_cutoff(168); // 0.66 (default)
if (x == "Slightly sensitive") { id(mww_hey_jarvis).set_probability_cutoff(247); // 0.97 (default)
id(mww_hey_jarvis).set_probability_cutoff(247); // 0.97 -> 0.563 FAPH on DipCo (Manifest's default) id(mww_hey_mycroft).set_probability_cutoff(242); // 0.95 (default)
id(mww_hey_mycroft).set_probability_cutoff(253); // 0.99 -> 0.567 FAPH on DipCo id(mww_okay_nabu).set_probability_cutoff(217); // 0.85 (default)
id(mww_okay_nabu).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default) id(mww_alexa).set_probability_cutoff(217); // 0.85 (default)
id(mww_alexa).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default) } else if (x == "More sensitive") {
} else if (x == "Moderately sensitive") { id(mww_computer).set_probability_cutoff(153); // 0.60
id(mww_hey_jarvis).set_probability_cutoff(235); // 0.92 -> 0.939 FAPH on DipCo id(mww_hey_jarvis).set_probability_cutoff(235); // 0.92
id(mww_hey_mycroft).set_probability_cutoff(242); // 0.95 -> 1.502 FAPH on DipCo (Manifest's default) id(mww_hey_mycroft).set_probability_cutoff(237); // 0.93
id(mww_okay_nabu).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo id(mww_okay_nabu).set_probability_cutoff(176); // 0.69
id(mww_alexa).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo id(mww_alexa).set_probability_cutoff(176); // 0.69
} else if (x == "Very sensitive") { } else if (x == "Very sensitive") {
id(mww_hey_jarvis).set_probability_cutoff(212); // 0.83 -> 1.502 FAPH on DipCo id(mww_computer).set_probability_cutoff(138); // 0.54
id(mww_hey_mycroft).set_probability_cutoff(237); // 0.93 -> 1.878 FAPH on DipCo id(mww_hey_jarvis).set_probability_cutoff(212); // 0.83
id(mww_okay_nabu).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo id(mww_hey_mycroft).set_probability_cutoff(230); // 0.90
id(mww_alexa).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo id(mww_okay_nabu).set_probability_cutoff(143); // 0.56
id(mww_alexa).set_probability_cutoff(143); // 0.56
} }