diff --git a/airgradient-basic.bin b/airgradient-basic.bin
index 4dff7eb..ef8a9b3 100644
Binary files a/airgradient-basic.bin and b/airgradient-basic.bin differ
diff --git a/airgradient-one.bin b/airgradient-one.bin
index 7b5093d..ecca8a0 100644
Binary files a/airgradient-one.bin and b/airgradient-one.bin differ
diff --git a/airgradient-open-air-o-1pst.bin b/airgradient-open-air-o-1pst.bin
index 86edef1..6b857ef 100644
Binary files a/airgradient-open-air-o-1pst.bin and b/airgradient-open-air-o-1pst.bin differ
diff --git a/airgradient-pro.bin b/airgradient-pro.bin
index b0703fc..9e6ae32 100644
Binary files a/airgradient-pro.bin and b/airgradient-pro.bin differ
diff --git a/full_config/ag-basic.yaml b/full_config/ag-basic.yaml
index d00a8f8..0b1c418 100644
--- a/full_config/ag-basic.yaml
+++ b/full_config/ag-basic.yaml
@@ -1,549 +1,562 @@
-substitutions:
-  devicename: ag-basic
-  friendly_devicename: AG Basic
-  ag_esphome_config_version: 1.0.0
-  add_mac_suffix: 'true'
-esphome:
-  name: ag-basic
-  friendly_name: AG Basic
-  name_add_mac_suffix: true
-  project:
-    name: mallocarray.airgradient
-    version: 1.0.0
-  min_version: 2023.7.0
-  on_boot:
-  - priority: 200.0
-    then:
-    - if:
-        condition:
-          switch.is_on:
-            id: upload_airgradient
-        then:
-        - http_request.post:
-            url: !lambda |-
-              return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
-            headers:
-              Content-Type: application/json
-            json:
-              wifi: !lambda |-
-                return to_string(-50);
-            verify_ssl: true
-            method: POST
-  build_path: build/ag-basic
-  area: ''
-  platformio_options: {}
-  includes: []
-  libraries: []
-esp8266:
-  board: d1_mini
-  restore_from_flash: true
-  framework:
-    version: 3.0.2
-    source: ~3.30002.0
-    platform_version: platformio/espressif8266@3.2.0
-  early_pin_init: true
-  board_flash_mode: dout
-logger:
-  logs:
-    component: ERROR
-  baud_rate: 115200
-  tx_buffer_size: 512
-  deassert_rts_dtr: false
-  hardware_uart: UART0
-  level: DEBUG
-  esp8266_store_log_strings_in_flash: true
-captive_portal: {}
-uart:
-- rx_pin:
-    number: 2
-    mode:
-      input: true
-      output: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  tx_pin:
-    number: 0
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  baud_rate: 9600
-  id: senseair_s8_uart
-  rx_buffer_size: 256
-  stop_bits: 1
-  data_bits: 8
-  parity: NONE
-- rx_pin:
-    number: 14
-    mode:
-      input: true
-      output: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  tx_pin:
-    number: 12
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  baud_rate: 9600
-  id: pms5003_uart
-  rx_buffer_size: 256
-  stop_bits: 1
-  data_bits: 8
-  parity: NONE
-i2c:
-- sda: 4
-  scl: 5
-  frequency: 400000.0
-  scan: true
-sensor:
-- platform: pmsx003
-  type: PMSX003
-  uart_id: pms5003_uart
-  pm_2_5:
-    name: PM 2.5
-    id: pm_2_5
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_1_0:
-    name: PM 1.0
-    id: pm_1_0
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_10_0:
-    name: PM 10.0
-    id: pm_10_0
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_0_3um:
-    name: PM 0.3
-    id: pm_0_3um
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: /dL
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-  update_interval: 0s
-- platform: template
-  name: PM 2.5 AQI
-  id: pm_2_5_aqi
-  update_interval: 5min
-  unit_of_measurement: AQI
-  icon: mdi:air-filter
-  accuracy_decimals: 0
-  filters:
-  - skip_initial: 10
-  lambda: !lambda |-
-    // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
-    // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
-    if (id(pm_2_5).state <= 12.0) {
-    // good
-    return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
-    } else if (id(pm_2_5).state <= 35.4) {
-    // moderate
-    return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
-    } else if (id(pm_2_5).state <= 55.4) {
-    // usg
-    return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
-    } else if (id(pm_2_5).state <= 150.4) {
-    // unhealthy
-    return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
-    } else if (id(pm_2_5).state <= 250.4) {
-    // very unhealthy
-    return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
-    } else if (id(pm_2_5).state <= 350.4) {
-    // hazardous
-    return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
-    } else if (id(pm_2_5).state <= 500.4) {
-    // hazardous 2
-    return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
-    } else {
-    return(500);
-    }
-  disabled_by_default: false
-  force_update: false
-- platform: senseair
-  co2:
-    name: CO2
-    id: co2
-    filters:
-    - skip_initial: 2
-    - clamp:
-        min_value: 400.0
-        max_value: .nan
-        ignore_out_of_range: false
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: ppm
-    icon: mdi:molecule-co2
-    accuracy_decimals: 0
-    device_class: carbon_dioxide
-    state_class: measurement
-  id: senseair_s8
-  uart_id: senseair_s8_uart
-  update_interval: 60s
-- platform: sht3xd
-  temperature:
-    name: Temperature
-    id: temp
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: °C
-    accuracy_decimals: 1
-    device_class: temperature
-    state_class: measurement
-  humidity:
-    name: Humidity
-    id: humidity
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: '%'
-    accuracy_decimals: 1
-    device_class: humidity
-    state_class: measurement
-  address: 0x44
-  heater_enabled: false
-  update_interval: 60s
-- platform: wifi_signal
-  name: WiFi Signal
-  id: wifi_dbm
-  update_interval: 60s
-  disabled_by_default: false
-  force_update: false
-  unit_of_measurement: dBm
-  accuracy_decimals: 0
-  device_class: signal_strength
-  state_class: measurement
-  entity_category: diagnostic
-- platform: uptime
-  name: Uptime
-  id: device_uptime
-  disabled_by_default: false
-  force_update: false
-  unit_of_measurement: s
-  icon: mdi:timer-outline
-  accuracy_decimals: 0
-  device_class: duration
-  state_class: total_increasing
-  entity_category: diagnostic
-  update_interval: 60s
-button:
-- platform: template
-  name: SenseAir S8 Calibration
-  id: senseair_s8_calibrate_button
-  on_press:
-  - then:
-    - senseair.background_calibration:
-        id: senseair_s8
-    - delay: 70s
-    - senseair.background_calibration_result:
-        id: senseair_s8
-  disabled_by_default: false
-- platform: template
-  name: SenseAir S8 Show Calibration Interval
-  id: senseair_s8_show_calibrate_interval
-  on_press:
-  - then:
-    - senseair.abc_get_period:
-        id: senseair_s8
-  disabled_by_default: false
-switch:
-- platform: template
-  name: SenseAir S8 Automatic Background Calibration
-  id: senseair_s8_abc_switch
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  entity_category: config
-  turn_on_action:
-    then:
-    - senseair.abc_enable:
-        id: senseair_s8
-  turn_off_action:
-    then:
-    - senseair.abc_disable:
-        id: senseair_s8
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Temperature in °F
-  icon: mdi:thermometer
-  id: display_in_f
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Upload to AirGradient Dashboard
-  id: upload_airgradient
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  disabled_by_default: false
-  assumed_state: false
-- platform: safe_mode
-  name: Flash Mode (Safe Mode)
-  icon: mdi:cellphone-arrow-down
-  disabled_by_default: false
-  restore_mode: ALWAYS_OFF
-  entity_category: config
-font:
-- file:
-    path: ./fonts/liberation_sans/LiberationSans-Regular.ttf
-    type: local
-  id: font1
-  size: 24
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-display:
-- platform: ssd1306_i2c
-  model: SSD1306_64X48
-  id: oled_display
-  reset_pin:
-    number: 16
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  address: 0x3C
-  pages:
-  - id: display_pm2
-    lambda: !lambda |-
-      it.print(0, 0, id(font1), "PM2");
-      it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f",id(pm_2_5).state);
-  - id: display_co2
-    lambda: !lambda |-
-      it.print(0, 0, id(font1), "CO2");
-      it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f",id(co2).state);
-  - id: display_tempf
-    lambda: !lambda |-
-      it.print(0, 0, id(font1), "°F");
-      it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.1f",id(temp).state*9/5+32);
-  - id: display_humidity
-    lambda: !lambda |-
-      it.print(0, 0, id(font1), "HUM");
-      it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f%%",id(humidity).state);
-  - id: boot
-    lambda: !lambda |-
-      it.printf(0, 0, id(font1), TextAlign::TOP_RIGHT, "%s", get_mac_address().substr(6,11).c_str());
-  on_page_change:
-  - to: boot
-    then:
-    - if:
-        condition:
-          lambda: !lambda |-
-            return id(device_uptime).state > 30;
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  auto_clear_enabled: true
-  brightness: 1.0
-  contrast: 1.0
-  flip_x: true
-  flip_y: true
-  offset_x: 0
-  offset_y: 0
-  invert: false
-  update_interval: 1s
-interval:
-- interval: 5s
-  then:
-  - if:
-      condition:
-        lambda: !lambda |-
-          return id(device_uptime).state < 10;
-      then:
-      - display.page.show:
-          id: boot
-      else:
-      - display.page.show_next:
-          id: oled_display
-      - component.update:
-          id: oled_display
-  startup_delay: 0s
-- interval: 150s
-  then:
-  - if:
-      condition:
-        switch.is_on:
-          id: upload_airgradient
-      then:
-      - http_request.post:
-          url: !lambda |-
-            return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
-          headers:
-            Content-Type: application/json
-          json:
-            wifi: !lambda |-
-              return to_string(id(wifi_dbm).state);
-            pm01: !lambda |-
-              return to_string(id(pm_1_0).state);
-            pm02: !lambda |-
-              return to_string(id(pm_2_5).state);
-            pm10: !lambda |-
-              return to_string(id(pm_10_0).state);
-            pm003_count: !lambda |-
-              return to_string(id(pm_0_3um).state);
-            rco2: !lambda |-
-              return to_string(id(co2).state);
-            atmp: !lambda |-
-              return to_string(id(temp).state);
-            rhum: !lambda |-
-              return to_string(id(humidity).state);
-          verify_ssl: true
-          method: POST
-  startup_delay: 0s
-http_request:
-  useragent: ESPHome
-  follow_redirects: true
-  redirect_limit: 3
-  timeout: 5s
-  esp8266_disable_ssl_support: false
-api:
-  port: 6053
-  password: \033[5m''\033[6m
-  reboot_timeout: 15min
-ota:
-  safe_mode: true
-  port: 8266
-  reboot_timeout: 5min
-  num_attempts: 10
-wifi:
-  ap:
-    ap_timeout: 1min
-  domain: .local
-  reboot_timeout: 15min
-  power_save_mode: NONE
-  fast_connect: false
-  output_power: 20.0
-  passive_scan: false
-  enable_on_boot: true
-  use_address: ag-basic.local
-
+substitutions:
+  name: ag-basic
+  friendly_name: AG Basic
+  config_version: 2.0.0
+  name_add_mac_suffix: 'false'
+esphome:
+  name: ag-basic
+  friendly_name: AG Basic
+  name_add_mac_suffix: false
+  project:
+    name: mallocarray.airgradient
+    version: 2.0.0
+  min_version: 2023.12.0
+  on_boot:
+  - priority: 200.0
+    then:
+    - if:
+        condition:
+          switch.is_on:
+            id: upload_airgradient
+        then:
+        - http_request.post:
+            url: !lambda |-
+              return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
+            headers:
+              Content-Type: application/json
+            json:
+              wifi: !lambda |-
+                return to_string(-50);
+            verify_ssl: true
+            method: POST
+  build_path: build/ag-basic
+  area: ''
+  platformio_options: {}
+  includes: []
+  libraries: []
+esp8266:
+  board: d1_mini
+  restore_from_flash: true
+  framework:
+    version: 3.0.2
+    source: ~3.30002.0
+    platform_version: platformio/espressif8266@3.2.0
+  early_pin_init: true
+  board_flash_mode: dout
+logger:
+  logs:
+    component: ERROR
+  baud_rate: 115200
+  tx_buffer_size: 512
+  deassert_rts_dtr: false
+  hardware_uart: UART0
+  level: DEBUG
+  esp8266_store_log_strings_in_flash: true
+captive_portal: {}
+uart:
+- rx_pin:
+    number: 2
+    mode:
+      input: true
+      output: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  tx_pin:
+    number: 0
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  baud_rate: 9600
+  id: senseair_s8_uart
+  rx_buffer_size: 256
+  stop_bits: 1
+  data_bits: 8
+  parity: NONE
+- rx_pin:
+    number: 14
+    mode:
+      input: true
+      output: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  tx_pin:
+    number: 12
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  baud_rate: 9600
+  id: pms5003_uart
+  rx_buffer_size: 256
+  stop_bits: 1
+  data_bits: 8
+  parity: NONE
+i2c:
+- sda: 4
+  scl: 5
+  frequency: 400000.0
+  scan: true
+button:
+- platform: factory_reset
+  disabled_by_default: true
+  name: Factory Reset ESP
+  id: factory_reset_all
+  icon: mdi:restart-alert
+  entity_category: config
+  device_class: restart
+- platform: template
+  name: SenseAir S8 Calibration
+  id: senseair_s8_calibrate_button
+  on_press:
+  - then:
+    - senseair.background_calibration:
+        id: senseair_s8
+    - delay: 70s
+    - senseair.background_calibration_result:
+        id: senseair_s8
+  disabled_by_default: false
+- platform: template
+  name: SenseAir S8 Show Calibration Interval
+  id: senseair_s8_show_calibrate_interval
+  on_press:
+  - then:
+    - senseair.abc_get_period:
+        id: senseair_s8
+  disabled_by_default: false
+sensor:
+- platform: pmsx003
+  type: PMSX003
+  uart_id: pms5003_uart
+  pm_2_5:
+    name: PM 2.5
+    id: pm_2_5
+    device_class: pm25
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_1_0:
+    name: PM 1.0
+    id: pm_1_0
+    device_class: pm1
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_10_0:
+    name: PM 10.0
+    id: pm_10_0
+    device_class: pm10
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_0_3um:
+    name: PM 0.3
+    id: pm_0_3um
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: /dL
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+  update_interval: 0s
+- platform: template
+  name: PM 2.5 AQI
+  id: pm_2_5_aqi
+  update_interval: 5min
+  unit_of_measurement: AQI
+  icon: mdi:air-filter
+  accuracy_decimals: 0
+  filters:
+  - skip_initial: 10
+  lambda: !lambda |-
+    // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
+    // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
+    if (id(pm_2_5).state <= 12.0) {
+    // good
+    return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
+    } else if (id(pm_2_5).state <= 35.4) {
+    // moderate
+    return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
+    } else if (id(pm_2_5).state <= 55.4) {
+    // usg
+    return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
+    } else if (id(pm_2_5).state <= 150.4) {
+    // unhealthy
+    return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
+    } else if (id(pm_2_5).state <= 250.4) {
+    // very unhealthy
+    return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
+    } else if (id(pm_2_5).state <= 350.4) {
+    // hazardous
+    return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
+    } else if (id(pm_2_5).state <= 500.4) {
+    // hazardous 2
+    return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
+    } else {
+    return(500);
+    }
+  disabled_by_default: false
+  force_update: false
+- platform: senseair
+  co2:
+    name: CO2
+    id: co2
+    filters:
+    - skip_initial: 2
+    - clamp:
+        min_value: 400.0
+        max_value: .nan
+        ignore_out_of_range: false
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: ppm
+    icon: mdi:molecule-co2
+    accuracy_decimals: 0
+    device_class: carbon_dioxide
+    state_class: measurement
+  id: senseair_s8
+  uart_id: senseair_s8_uart
+  update_interval: 60s
+- platform: sht3xd
+  temperature:
+    name: Temperature
+    id: temp
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: °C
+    accuracy_decimals: 1
+    device_class: temperature
+    state_class: measurement
+  humidity:
+    name: Humidity
+    id: humidity
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: '%'
+    accuracy_decimals: 1
+    device_class: humidity
+    state_class: measurement
+  address: 0x44
+  heater_enabled: false
+  update_interval: 60s
+- platform: wifi_signal
+  name: WiFi Signal
+  id: wifi_dbm
+  update_interval: 60s
+  disabled_by_default: false
+  force_update: false
+  unit_of_measurement: dBm
+  accuracy_decimals: 0
+  device_class: signal_strength
+  state_class: measurement
+  entity_category: diagnostic
+- platform: uptime
+  name: Uptime
+  id: device_uptime
+  disabled_by_default: false
+  force_update: false
+  unit_of_measurement: s
+  icon: mdi:timer-outline
+  accuracy_decimals: 0
+  device_class: duration
+  state_class: total_increasing
+  entity_category: diagnostic
+  update_interval: 60s
+switch:
+- platform: template
+  name: SenseAir S8 Automatic Background Calibration
+  id: senseair_s8_abc_switch
+  restore_mode: RESTORE_DEFAULT_ON
+  optimistic: true
+  entity_category: config
+  turn_on_action:
+    then:
+    - senseair.abc_enable:
+        id: senseair_s8
+  turn_off_action:
+    then:
+    - senseair.abc_disable:
+        id: senseair_s8
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Temperature in °F
+  icon: mdi:thermometer
+  id: display_in_f
+  restore_mode: RESTORE_DEFAULT_ON
+  optimistic: true
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Upload to AirGradient Dashboard
+  id: upload_airgradient
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  disabled_by_default: false
+  assumed_state: false
+- platform: safe_mode
+  name: Flash Mode (Safe Mode)
+  icon: mdi:cellphone-arrow-down
+  disabled_by_default: false
+  restore_mode: ALWAYS_OFF
+  entity_category: config
+font:
+- file:
+    path: ./fonts/liberation_sans/LiberationSans-Regular.ttf
+    type: local
+  id: font1
+  size: 24
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+display:
+- platform: ssd1306_i2c
+  model: SSD1306_64X48
+  id: oled_display
+  reset_pin:
+    number: 16
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  address: 0x3C
+  pages:
+  - id: display_pm2
+    lambda: !lambda |-
+      it.print(0, 0, id(font1), "PM2");
+      it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f",id(pm_2_5).state);
+  - id: display_co2
+    lambda: !lambda |-
+      it.print(0, 0, id(font1), "CO2");
+      it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f",id(co2).state);
+  - id: display_tempf
+    lambda: !lambda |-
+      it.print(0, 0, id(font1), "°F");
+      it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.1f",id(temp).state*9/5+32);
+  - id: display_humidity
+    lambda: !lambda |-
+      it.print(0, 0, id(font1), "HUM");
+      it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f%%",id(humidity).state);
+  - id: boot
+    lambda: !lambda |-
+      it.printf(0, 0, id(font1), TextAlign::TOP_RIGHT, "%s", get_mac_address().substr(6,11).c_str());
+  on_page_change:
+  - to: boot
+    then:
+    - if:
+        condition:
+          lambda: !lambda |-
+            return id(device_uptime).state > 30;
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  auto_clear_enabled: true
+  brightness: 1.0
+  contrast: 1.0
+  flip_x: true
+  flip_y: true
+  offset_x: 0
+  offset_y: 0
+  invert: false
+  update_interval: 1s
+interval:
+- interval: 5s
+  then:
+  - if:
+      condition:
+        lambda: !lambda |-
+          return id(device_uptime).state < 10;
+      then:
+      - display.page.show:
+          id: boot
+      else:
+      - display.page.show_next:
+          id: oled_display
+      - component.update:
+          id: oled_display
+  startup_delay: 0s
+- interval: 150s
+  then:
+  - if:
+      condition:
+        switch.is_on:
+          id: upload_airgradient
+      then:
+      - http_request.post:
+          url: !lambda |-
+            return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
+          headers:
+            Content-Type: application/json
+          json:
+            wifi: !lambda |-
+              return to_string(id(wifi_dbm).state);
+            pm01: !lambda |-
+              return to_string(id(pm_1_0).state);
+            pm02: !lambda |-
+              return to_string(id(pm_2_5).state);
+            pm10: !lambda |-
+              return to_string(id(pm_10_0).state);
+            pm003_count: !lambda |-
+              return to_string(id(pm_0_3um).state);
+            rco2: !lambda |-
+              return to_string(id(co2).state);
+            atmp: !lambda |-
+              return to_string(id(temp).state);
+            rhum: !lambda |-
+              return to_string(id(humidity).state);
+          verify_ssl: true
+          method: POST
+  startup_delay: 0s
+http_request:
+  useragent: ESPHome
+  follow_redirects: true
+  redirect_limit: 3
+  timeout: 5s
+  esp8266_disable_ssl_support: false
+api:
+  port: 6053
+  password: \033[5m''\033[6m
+  reboot_timeout: 15min
+ota:
+  safe_mode: true
+  port: 8266
+  reboot_timeout: 5min
+  num_attempts: 10
+wifi:
+  ap:
+    ap_timeout: 1min
+  domain: .local
+  reboot_timeout: 15min
+  power_save_mode: NONE
+  fast_connect: false
+  output_power: 20.0
+  passive_scan: false
+  enable_on_boot: true
+  use_address: ag-basic.local
+dashboard_import:
+  package_import_url: github://MallocArray/airgradient_esphome/airgradient-basic.yaml
+  import_full_config: false
+
diff --git a/full_config/ag-one.yaml b/full_config/ag-one.yaml
index 8c0e814..2f9cc52 100644
--- a/full_config/ag-one.yaml
+++ b/full_config/ag-one.yaml
@@ -1,1442 +1,1472 @@
-substitutions:
-  devicename: ag-one
-  friendly_devicename: AG One
-  ag_esphome_config_version: 1.0.0
-  add_mac_suffix: 'true'
-esphome:
-  name: ag-one
-  friendly_name: AG One
-  name_add_mac_suffix: true
-  project:
-    name: mallocarray.airgradient
-    version: 1.0.0
-  min_version: 2023.7.0
-  on_boot:
-  - priority: 200.0
-    then:
-    - if:
-        condition:
-          switch.is_on:
-            id: upload_airgradient
-        then:
-        - http_request.post:
-            url: !lambda |-
-              return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures";
-            headers:
-              Content-Type: application/json
-            json:
-              wifi: !lambda |-
-                return to_string(-50);
-            verify_ssl: true
-            method: POST
-  build_path: build/ag-one
-  area: ''
-  platformio_options: {}
-  includes: []
-  libraries: []
-esp32:
-  board: esp32-c3-devkitm-1
-  flash_size: 4MB
-  framework:
-    version: 2.0.5
-    source: ~3.20005.0
-    platform_version: platformio/espressif32@5.4.0
-    type: arduino
-  variant: ESP32C3
-logger:
-  baud_rate: 0
-  logs:
-    component: ERROR
-  tx_buffer_size: 512
-  deassert_rts_dtr: false
-  hardware_uart: UART0
-  level: DEBUG
-captive_portal: {}
-uart:
-- rx_pin:
-    number: 0
-    mode:
-      input: true
-      output: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    ignore_strapping_warning: false
-    drive_strength: 20.0
-  tx_pin:
-    number: 1
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    ignore_strapping_warning: false
-    drive_strength: 20.0
-  baud_rate: 9600
-  id: senseair_s8_uart
-  rx_buffer_size: 256
-  stop_bits: 1
-  data_bits: 8
-  parity: NONE
-- rx_pin:
-    number: 20
-    mode:
-      input: true
-      output: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    ignore_strapping_warning: false
-    drive_strength: 20.0
-  tx_pin:
-    number: 21
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    ignore_strapping_warning: false
-    drive_strength: 20.0
-  baud_rate: 9600
-  id: pms5003_uart
-  rx_buffer_size: 256
-  stop_bits: 1
-  data_bits: 8
-  parity: NONE
-i2c:
-- sda: 7
-  scl: 6
-  frequency: 400000.0
-  scan: true
-sensor:
-- platform: pmsx003
-  type: PMSX003
-  uart_id: pms5003_uart
-  pm_2_5:
-    name: PM 2.5
-    id: pm_2_5
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_1_0:
-    name: PM 1.0
-    id: pm_1_0
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_10_0:
-    name: PM 10.0
-    id: pm_10_0
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_0_3um:
-    name: PM 0.3
-    id: pm_0_3um
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: /dL
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-  update_interval: 0s
-- platform: template
-  name: PM 2.5 AQI
-  id: pm_2_5_aqi
-  update_interval: 5min
-  unit_of_measurement: AQI
-  icon: mdi:air-filter
-  accuracy_decimals: 0
-  filters:
-  - skip_initial: 10
-  lambda: !lambda |-
-    // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
-    // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
-    if (id(pm_2_5).state <= 12.0) {
-    // good
-    return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
-    } else if (id(pm_2_5).state <= 35.4) {
-    // moderate
-    return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
-    } else if (id(pm_2_5).state <= 55.4) {
-    // usg
-    return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
-    } else if (id(pm_2_5).state <= 150.4) {
-    // unhealthy
-    return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
-    } else if (id(pm_2_5).state <= 250.4) {
-    // very unhealthy
-    return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
-    } else if (id(pm_2_5).state <= 350.4) {
-    // hazardous
-    return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
-    } else if (id(pm_2_5).state <= 500.4) {
-    // hazardous 2
-    return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
-    } else {
-    return(500);
-    }
-  disabled_by_default: false
-  force_update: false
-- platform: senseair
-  co2:
-    name: CO2
-    id: co2
-    filters:
-    - skip_initial: 2
-    - clamp:
-        min_value: 400.0
-        max_value: .nan
-        ignore_out_of_range: false
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: ppm
-    icon: mdi:molecule-co2
-    accuracy_decimals: 0
-    device_class: carbon_dioxide
-    state_class: measurement
-  id: senseair_s8
-  uart_id: senseair_s8_uart
-  update_interval: 60s
-- platform: sht4x
-  temperature:
-    name: Temperature
-    id: temp
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: °C
-    icon: mdi:thermometer
-    accuracy_decimals: 2
-    device_class: temperature
-    state_class: measurement
-  humidity:
-    name: Humidity
-    id: humidity
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: '%'
-    icon: mdi:water-percent
-    accuracy_decimals: 2
-    device_class: humidity
-    state_class: measurement
-  address: 0x44
-  precision: High
-  heater_power: High
-  heater_time: Long
-  heater_max_duty: 0.0
-  update_interval: 60s
-- platform: sgp4x
-  voc:
-    name: VOC Index
-    id: voc
-    disabled_by_default: false
-    force_update: false
-    icon: mdi:radiator
-    accuracy_decimals: 0
-    device_class: aqi
-    state_class: measurement
-  nox:
-    name: NOx Index
-    id: nox
-    disabled_by_default: false
-    force_update: false
-    icon: mdi:radiator
-    accuracy_decimals: 0
-    device_class: aqi
-    state_class: measurement
-  compensation:
-    temperature_source: temp
-    humidity_source: humidity
-  store_baseline: true
-  update_interval: 60s
-  address: 0x59
-- platform: wifi_signal
-  name: WiFi Signal
-  id: wifi_dbm
-  update_interval: 60s
-  disabled_by_default: false
-  force_update: false
-  unit_of_measurement: dBm
-  accuracy_decimals: 0
-  device_class: signal_strength
-  state_class: measurement
-  entity_category: diagnostic
-- platform: uptime
-  name: Uptime
-  id: device_uptime
-  disabled_by_default: false
-  force_update: false
-  unit_of_measurement: s
-  icon: mdi:timer-outline
-  accuracy_decimals: 0
-  device_class: duration
-  state_class: total_increasing
-  entity_category: diagnostic
-  update_interval: 60s
-button:
-- platform: template
-  name: SenseAir S8 Calibration
-  id: senseair_s8_calibrate_button
-  on_press:
-  - then:
-    - senseair.background_calibration:
-        id: senseair_s8
-    - delay: 70s
-    - senseair.background_calibration_result:
-        id: senseair_s8
-  disabled_by_default: false
-- platform: template
-  name: SenseAir S8 Show Calibration Interval
-  id: senseair_s8_show_calibrate_interval
-  on_press:
-  - then:
-    - senseair.abc_get_period:
-        id: senseair_s8
-  disabled_by_default: false
-switch:
-- platform: template
-  name: SenseAir S8 Automatic Background Calibration
-  id: senseair_s8_abc_switch
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  entity_category: config
-  turn_on_action:
-    then:
-    - senseair.abc_enable:
-        id: senseair_s8
-  turn_off_action:
-    then:
-    - senseair.abc_disable:
-        id: senseair_s8
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Temperature in °F
-  id: display_in_f
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  entity_category: config
-  icon: mdi:thermometer
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display AirGradient Default Page
-  id: display_ag_default_page
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  entity_category: config
-  icon: mdi:monitor
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Summary Pages
-  id: display_summary_pages
-  restore_mode: RESTORE_DEFAULT_OFF
-  optimistic: true
-  entity_category: config
-  icon: mdi:monitor
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Air Quality Page
-  id: display_air_quality_page
-  restore_mode: RESTORE_DEFAULT_OFF
-  optimistic: true
-  entity_category: config
-  icon: mdi:monitor
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Temp/Hum Page
-  id: display_air_temp_page
-  restore_mode: RESTORE_DEFAULT_OFF
-  optimistic: true
-  entity_category: config
-  icon: mdi:monitor
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display VOC Page
-  id: display_voc_page
-  restore_mode: RESTORE_DEFAULT_OFF
-  optimistic: true
-  entity_category: config
-  icon: mdi:monitor
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Combo Page
-  id: display_combo_page
-  restore_mode: RESTORE_DEFAULT_OFF
-  optimistic: true
-  entity_category: config
-  icon: mdi:monitor
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Boot Page
-  id: display_boot_page
-  restore_mode: ALWAYS_ON
-  optimistic: true
-  entity_category: config
-  icon: mdi:monitor
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Blank Page
-  id: display_blank_page
-  restore_mode: RESTORE_DEFAULT_OFF
-  optimistic: true
-  entity_category: config
-  icon: mdi:monitor
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Upload to AirGradient Dashboard
-  id: upload_airgradient
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  disabled_by_default: false
-  assumed_state: false
-- platform: safe_mode
-  name: Flash Mode (Safe Mode)
-  icon: mdi:cellphone-arrow-down
-  disabled_by_default: false
-  restore_mode: ALWAYS_OFF
-  entity_category: config
-font:
-- file:
-    family: Open Sans
-    weight: 400
-    italic: false
-    type: gfonts
-  id: open_sans_14
-  size: 14
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-- file:
-    family: Open Sans
-    weight: 400
-    italic: false
-    type: gfonts
-  id: open_sans_9
-  size: 9
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-- file:
-    family: Open Sans
-    weight: 400
-    italic: false
-    type: gfonts
-  id: open_sans_20
-  size: 20
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-- file:
-    family: Poppins
-    weight: 300
-    italic: false
-    type: gfonts
-  id: poppins_light
-  size: 14
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-- file:
-    family: Poppins
-    weight: 300
-    italic: false
-    type: gfonts
-  id: poppins_light_12
-  size: 12
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-- file:
-    family: Ubuntu Mono
-    weight: 400
-    italic: false
-    type: gfonts
-  id: ubuntu
-  size: 22
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-display:
-- platform: ssd1306_i2c
-  model: SH1106_128X64
-  id: oled_display
-  address: 0x3C
-  pages:
-  - id: airgradient_default
-    lambda: !lambda |-
-      if (id(display_in_f).state) {
-        it.printf(0, 0, id(open_sans_14), "%.1f°F", id(temp).state*9/5+32);
-      } else {
-        it.printf(0, 0, id(open_sans_14), "%.1f°C", id(temp).state);
-      }
-      it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
-      it.line(0,17,128,17);
-      it.printf(0,19, id(open_sans_9), "CO2");
-      it.printf(0,27, id(open_sans_20), "%.0f", id(co2).state);
-      it.printf(0,52, id(open_sans_9), "ppm");
-      it.line(50,19,50,64);
-      it.printf(54, 19, id(open_sans_9), "PM2.5");
-      it.printf(54, 27, id(open_sans_20), "%.0f", id(pm_2_5).state);
-      it.printf(54, 52, id(open_sans_9), "µg/m³");
-      it.line(100,19,100,64);
-      it.printf(104,18, id(open_sans_9), "TVOC");
-      it.printf(104,29, id(open_sans_9), "%.0f", id(voc).state);
-      it.printf(104,41, id(open_sans_9), "NOx");
-      it.printf(104,52, id(open_sans_9), "%.0f", id(nox).state);
-  - id: boot
-    lambda: !lambda |-
-      it.printf(0, 0, id(open_sans_14), "ID:");
-      it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%s", get_mac_address().c_str());
-      it.printf(0, 21, id(open_sans_14), "Config Ver: 1.0.0");
-      it.printf(0, 42, id(open_sans_14), "AG One");
-  - id: summary1
-    lambda: !lambda |-
-      it.printf(0, 0, id(poppins_light), "CO2:");
-      it.printf(128, 0, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
-      it.printf(0, 16, id(poppins_light), "PM2.5:");
-      it.printf(128, 16, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f µg/m³", id(pm_2_5).state);
-      it.printf(0, 32, id(poppins_light), "Temp:");
-      if (id(display_in_f).state) {
-        it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f°F", id(temp).state*9/5+32);
-      } else {
-        it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f°C", id(temp).state);
-      }
-      it.printf(0, 48, id(poppins_light), "Humidity:");
-      it.printf(128, 48, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
-  - id: summary2
-    lambda: !lambda |-
-      it.printf(0, 0, id(poppins_light), "CO2:");
-      it.printf(128, 0, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
-      it.printf(0, 16, id(poppins_light), "PM2.5:");
-      it.printf(128, 16, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f µg/m³", id(pm_2_5).state);
-      it.printf(0, 32, id(poppins_light), "VOC:");
-      it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f", id(voc).state);
-      it.printf(0, 48, id(poppins_light), "NOx:");
-      it.printf(128, 48, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f", id(nox).state);
-  - id: air_quality
-    lambda: !lambda |-
-      it.printf(0, 6, id(ubuntu), "CO2");
-      it.printf(128, 6, id(ubuntu), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
-      it.printf(0, 34, id(ubuntu), "PM2");
-      it.printf(128, 34, id(ubuntu), TextAlign::TOP_RIGHT, "%.0f µg/m³", id(pm_2_5).state);
-  - id: air_temp
-    lambda: !lambda |-
-      it.printf(0, 6, id(ubuntu), "Temp");
-      if (id(display_in_f).state) {
-        it.printf(128, 6, id(ubuntu), TextAlign::TOP_RIGHT, "%.1f°F", id(temp).state*9/5+32);
-      } else {
-        it.printf(128, 6, id(ubuntu), TextAlign::TOP_RIGHT, "%.1f°C", id(temp).state);
-      }
-      it.printf(0, 34, id(ubuntu), "Humid");
-      it.printf(128, 34, id(ubuntu), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
-  - id: air_voc
-    lambda: !lambda |-
-      it.printf(0, 6, id(ubuntu), "VOC:");
-      it.printf(128, 6, id(ubuntu), TextAlign::TOP_RIGHT, "%.0f", id(voc).state);
-      it.printf(0, 34, id(ubuntu), "NOx:");
-      it.printf(128, 34, id(ubuntu), TextAlign::TOP_RIGHT, "%.0f", id(nox).state);
-  - id: combo
-    lambda: !lambda |-
-      if (id(display_in_f).state) {
-        it.printf(0, 0, id(poppins_light_12), "%.1f °F", id(temp).state*9/5+32);
-      } else {
-        it.printf(0, 0, id(poppins_light_12), "%.1f °C", id(temp).state);
-      }
-      it.printf(128, 0, id(poppins_light_12), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
-      it.printf(0, 16, id(poppins_light_12), "%.0f µg", id(pm_2_5).state);
-      it.printf(128, 16, id(poppins_light_12), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
-      it.printf(0, 32, id(poppins_light_12), "VOC: %.0f", id(voc).state);
-      it.printf(128, 32, id(poppins_light_12), TextAlign::TOP_RIGHT, "NOx: %.0f", id(nox).state);
-      it.printf(0, 48, id(poppins_light_12), "AQI: %.0f", id(pm_2_5_aqi).state);
-  - id: blank
-    lambda: !lambda |-
-      it.printf(0, 0, id(open_sans_14), "");
-  on_page_change:
-  - to: airgradient_default
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_ag_default_page
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  - to: summary1
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_summary_pages
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  - to: summary2
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_summary_pages
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  - to: air_quality
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_air_quality_page
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  - to: air_temp
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_air_temp_page
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  - to: air_voc
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_voc_page
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  - to: combo
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_combo_page
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  - to: boot
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_boot_page
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  - to: blank
-    then:
-    - if:
-        condition:
-          switch.is_off:
-            id: display_blank_page
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  auto_clear_enabled: true
-  brightness: 1.0
-  contrast: 1.0
-  flip_x: true
-  flip_y: true
-  offset_x: 0
-  offset_y: 0
-  invert: false
-  update_interval: 1s
-interval:
-- interval: 10s
-  startup_delay: 1s
-  then:
-  - if:
-      condition:
-        switch.is_on:
-          id: display_boot_page
-      then:
-      - switch.turn_off:
-          id: display_boot_page
-- interval: 5s
-  then:
-  - if:
-      condition:
-        switch.is_on:
-          id: display_boot_page
-      then:
-      - display.page.show:
-          id: boot
-      else:
-      - display.page.show_next:
-          id: oled_display
-      - component.update:
-          id: oled_display
-  startup_delay: 0s
-- interval: 1min
-  then:
-  - if:
-      condition:
-        light.is_on:
-          id: led_strip
-      then:
-      - if:
-          condition:
-            lambda: !lambda |-
-              return id(co2).state < 800;
-          then:
-          - light.turn_on:
-              id: led_strip
-              brightness: !lambda |-
-                return id(led_brightness).state / 100.0;
-              red: 0.0
-              green: 1.0
-              blue: 0.0
-              state: true
-      - if:
-          condition:
-            lambda: !lambda |-
-              return id(co2).state >= 800 && id(co2).state < 1000;
-          then:
-          - light.turn_on:
-              id: led_strip
-              brightness: !lambda |-
-                return id(led_brightness).state / 100.0;
-              red: 1.0
-              green: 1.0
-              blue: 0.0
-              state: true
-      - if:
-          condition:
-            lambda: !lambda |-
-              return id(co2).state >= 1000 && id(co2).state < 1500;
-          then:
-          - light.turn_on:
-              id: led_strip
-              brightness: !lambda |-
-                return id(led_brightness).state / 100.0;
-              red: 1.0
-              green: 0.7
-              blue: 0.0
-              state: true
-      - if:
-          condition:
-            lambda: !lambda |-
-              return id(co2).state >= 1500 && id(co2).state < 2000;
-          then:
-          - light.turn_on:
-              id: led_strip
-              brightness: !lambda |-
-                return id(led_brightness).state / 100.0;
-              red: 1.0
-              green: 0.0
-              blue: 0.0
-              state: true
-      - if:
-          condition:
-            lambda: !lambda |-
-              return id(co2).state >= 2000 && id(co2).state < 3000;
-          then:
-          - light.turn_on:
-              id: led_strip
-              brightness: !lambda |-
-                return id(led_brightness).state / 100.0;
-              red: 0.6
-              green: 0.0
-              blue: 0.6
-              state: true
-      - if:
-          condition:
-            lambda: !lambda |-
-              return id(co2).state >= 3000 && id(co2).state < 10000;
-          then:
-          - light.turn_on:
-              id: led_strip
-              brightness: !lambda |-
-                return id(led_brightness).state / 100.0;
-              red: 0.4
-              green: 0.0
-              blue: 0.0
-              state: true
-  startup_delay: 0s
-- interval: 150s
-  then:
-  - if:
-      condition:
-        switch.is_on:
-          id: upload_airgradient
-      then:
-      - http_request.post:
-          url: !lambda |-
-            return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures";
-          headers:
-            Content-Type: application/json
-          json:
-            wifi: !lambda |-
-              return to_string(id(wifi_dbm).state);
-            pm01: !lambda |-
-              return to_string(id(pm_1_0).state);
-            pm02: !lambda |-
-              return to_string(id(pm_2_5).state);
-            pm10: !lambda |-
-              return to_string(id(pm_10_0).state);
-            pm003_count: !lambda |-
-              return to_string(id(pm_0_3um).state);
-            rco2: !lambda |-
-              return to_string(id(co2).state);
-            atmp: !lambda |-
-              return to_string(id(temp).state);
-            rhum: !lambda |-
-              return to_string(id(humidity).state);
-            tvoc_index: !lambda |-
-              return to_string(id(voc).state);
-            nox_index: !lambda |-
-              return to_string(id(nox).state);
-          verify_ssl: true
-          method: POST
-  startup_delay: 0s
-- interval: 150s
-  then:
-  - output.turn_on:
-      id: watchdog
-  - delay: 20ms
-  - output.turn_off:
-      id: watchdog
-  startup_delay: 0s
-light:
-- platform: esp32_rmt_led_strip
-  rgb_order: GRB
-  pin: 10
-  num_leds: 11
-  rmt_channel: 0
-  chipset: WS2812
-  name: LED Strip
-  id: led_strip
-  restore_mode: RESTORE_DEFAULT_OFF
-  disabled_by_default: false
-  gamma_correct: 2.8
-  default_transition_length: 1s
-  flash_transition_length: 0s
-  is_rgbw: false
-number:
-- platform: template
-  name: LED Brightness %
-  icon: mdi:lightbulb
-  id: led_brightness
-  min_value: 0.0
-  max_value: 100.0
-  step: 1.0
-  initial_value: 100.0
-  optimistic: true
-  restore_value: true
-  mode: SLIDER
-  disabled_by_default: false
-  update_interval: 60s
-http_request:
-  useragent: ESPHome
-  follow_redirects: true
-  redirect_limit: 3
-  timeout: 5s
-output:
-- platform: gpio
-  id: watchdog
-  pin:
-    number: 2
-    ignore_strapping_warning: true
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    drive_strength: 20.0
-binary_sensor:
-- platform: gpio
-  pin:
-    number: 9
-    mode:
-      input: true
-      pullup: true
-      output: false
-      open_drain: false
-      pulldown: false
-    inverted: true
-    ignore_strapping_warning: true
-    drive_strength: 20.0
-  internal: true
-  name: Configuration Button
-  id: config_button
-  on_multi_click:
-  - timing:
-    - state: true
-      min_length: 0ms
-      max_length: 1s
-    - state: false
-      min_length: 500ms
-    then:
-    - logger.log:
-        format: Toggling display betwen C and F
-        tag: main
-        level: DEBUG
-        args: []
-    - switch.toggle:
-        id: display_in_f
-    invalid_cooldown: 1s
-  - timing:
-    - state: true
-      min_length: 1s
-      max_length: 5s
-    - state: false
-      min_length: 500ms
-    then:
-    - logger.log:
-        format: Starting manual CO2 calibration
-        tag: main
-        level: DEBUG
-        args: []
-    - senseair.background_calibration:
-        id: senseair_s8
-    - delay: 70s
-    - senseair.background_calibration_result:
-        id: senseair_s8
-    invalid_cooldown: 1s
-  disabled_by_default: false
-api:
-  port: 6053
-  password: \033[5m''\033[6m
-  reboot_timeout: 15min
-ota:
-  safe_mode: true
-  port: 3232
-  reboot_timeout: 5min
-  num_attempts: 10
-wifi:
-  ap:
-    ap_timeout: 1min
-  domain: .local
-  reboot_timeout: 15min
-  power_save_mode: LIGHT
-  fast_connect: false
-  passive_scan: false
-  enable_on_boot: true
-  use_address: ag-one.local
-
+substitutions:
+  name: ag-one
+  friendly_name: AG One
+  config_version: 2.0.0
+  name_add_mac_suffix: 'false'
+esphome:
+  name: ag-one
+  friendly_name: AG One
+  name_add_mac_suffix: false
+  project:
+    name: mallocarray.airgradient
+    version: 2.0.0
+  min_version: 2023.12.0
+  on_boot:
+  - priority: 200.0
+    then:
+    - if:
+        condition:
+          switch.is_on:
+            id: upload_airgradient
+        then:
+        - http_request.post:
+            url: !lambda |-
+              return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures";
+            headers:
+              Content-Type: application/json
+            json:
+              wifi: !lambda |-
+                return to_string(-50);
+            verify_ssl: true
+            method: POST
+  build_path: build/ag-one
+  area: ''
+  platformio_options: {}
+  includes: []
+  libraries: []
+esp32:
+  board: esp32-c3-devkitm-1
+  flash_size: 4MB
+  framework:
+    version: 2.0.5
+    source: ~3.20005.0
+    platform_version: platformio/espressif32@5.4.0
+    type: arduino
+  variant: ESP32C3
+logger:
+  baud_rate: 0
+  logs:
+    component: ERROR
+  tx_buffer_size: 512
+  deassert_rts_dtr: false
+  hardware_uart: UART0
+  level: DEBUG
+captive_portal: {}
+uart:
+- rx_pin:
+    number: 0
+    mode:
+      input: true
+      output: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    ignore_strapping_warning: false
+    drive_strength: 20.0
+  tx_pin:
+    number: 1
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    ignore_strapping_warning: false
+    drive_strength: 20.0
+  baud_rate: 9600
+  id: senseair_s8_uart
+  rx_buffer_size: 256
+  stop_bits: 1
+  data_bits: 8
+  parity: NONE
+- rx_pin:
+    number: 20
+    mode:
+      input: true
+      output: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    ignore_strapping_warning: false
+    drive_strength: 20.0
+  tx_pin:
+    number: 21
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    ignore_strapping_warning: false
+    drive_strength: 20.0
+  baud_rate: 9600
+  id: pms5003_uart
+  rx_buffer_size: 256
+  stop_bits: 1
+  data_bits: 8
+  parity: NONE
+i2c:
+- sda: 7
+  scl: 6
+  frequency: 400000.0
+  scan: true
+button:
+- platform: factory_reset
+  disabled_by_default: true
+  name: Factory Reset ESP
+  id: factory_reset_all
+  icon: mdi:restart-alert
+  entity_category: config
+  device_class: restart
+- platform: template
+  name: SenseAir S8 Calibration
+  id: senseair_s8_calibrate_button
+  on_press:
+  - then:
+    - senseair.background_calibration:
+        id: senseair_s8
+    - delay: 70s
+    - senseair.background_calibration_result:
+        id: senseair_s8
+  disabled_by_default: false
+- platform: template
+  name: SenseAir S8 Show Calibration Interval
+  id: senseair_s8_show_calibrate_interval
+  on_press:
+  - then:
+    - senseair.abc_get_period:
+        id: senseair_s8
+  disabled_by_default: false
+sensor:
+- platform: pmsx003
+  type: PMSX003
+  uart_id: pms5003_uart
+  pm_2_5:
+    name: PM 2.5
+    id: pm_2_5
+    device_class: pm25
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_1_0:
+    name: PM 1.0
+    id: pm_1_0
+    device_class: pm1
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_10_0:
+    name: PM 10.0
+    id: pm_10_0
+    device_class: pm10
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_0_3um:
+    name: PM 0.3
+    id: pm_0_3um
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: /dL
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+  update_interval: 0s
+- platform: template
+  name: PM 2.5 AQI
+  id: pm_2_5_aqi
+  update_interval: 5min
+  unit_of_measurement: AQI
+  icon: mdi:air-filter
+  accuracy_decimals: 0
+  filters:
+  - skip_initial: 10
+  lambda: !lambda |-
+    // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
+    // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
+    if (id(pm_2_5).state <= 12.0) {
+    // good
+    return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
+    } else if (id(pm_2_5).state <= 35.4) {
+    // moderate
+    return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
+    } else if (id(pm_2_5).state <= 55.4) {
+    // usg
+    return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
+    } else if (id(pm_2_5).state <= 150.4) {
+    // unhealthy
+    return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
+    } else if (id(pm_2_5).state <= 250.4) {
+    // very unhealthy
+    return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
+    } else if (id(pm_2_5).state <= 350.4) {
+    // hazardous
+    return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
+    } else if (id(pm_2_5).state <= 500.4) {
+    // hazardous 2
+    return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
+    } else {
+    return(500);
+    }
+  disabled_by_default: false
+  force_update: false
+- platform: senseair
+  co2:
+    name: CO2
+    id: co2
+    filters:
+    - skip_initial: 2
+    - clamp:
+        min_value: 400.0
+        max_value: .nan
+        ignore_out_of_range: false
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: ppm
+    icon: mdi:molecule-co2
+    accuracy_decimals: 0
+    device_class: carbon_dioxide
+    state_class: measurement
+  id: senseair_s8
+  uart_id: senseair_s8_uart
+  update_interval: 60s
+- platform: sht4x
+  temperature:
+    name: Temperature
+    id: temp
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: °C
+    icon: mdi:thermometer
+    accuracy_decimals: 2
+    device_class: temperature
+    state_class: measurement
+  humidity:
+    name: Humidity
+    id: humidity
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: '%'
+    icon: mdi:water-percent
+    accuracy_decimals: 2
+    device_class: humidity
+    state_class: measurement
+  address: 0x44
+  precision: High
+  heater_power: High
+  heater_time: Long
+  heater_max_duty: 0.0
+  update_interval: 60s
+- platform: sgp4x
+  voc:
+    name: VOC Index
+    id: voc
+    disabled_by_default: false
+    force_update: false
+    icon: mdi:radiator
+    accuracy_decimals: 0
+    device_class: aqi
+    state_class: measurement
+  nox:
+    name: NOx Index
+    id: nox
+    disabled_by_default: false
+    force_update: false
+    icon: mdi:radiator
+    accuracy_decimals: 0
+    device_class: aqi
+    state_class: measurement
+  compensation:
+    temperature_source: temp
+    humidity_source: humidity
+  store_baseline: true
+  update_interval: 60s
+  address: 0x59
+- platform: wifi_signal
+  name: WiFi Signal
+  id: wifi_dbm
+  update_interval: 60s
+  disabled_by_default: false
+  force_update: false
+  unit_of_measurement: dBm
+  accuracy_decimals: 0
+  device_class: signal_strength
+  state_class: measurement
+  entity_category: diagnostic
+- platform: uptime
+  name: Uptime
+  id: device_uptime
+  disabled_by_default: false
+  force_update: false
+  unit_of_measurement: s
+  icon: mdi:timer-outline
+  accuracy_decimals: 0
+  device_class: duration
+  state_class: total_increasing
+  entity_category: diagnostic
+  update_interval: 60s
+switch:
+- platform: template
+  name: SenseAir S8 Automatic Background Calibration
+  id: senseair_s8_abc_switch
+  restore_mode: RESTORE_DEFAULT_ON
+  optimistic: true
+  entity_category: config
+  turn_on_action:
+    then:
+    - senseair.abc_enable:
+        id: senseair_s8
+  turn_off_action:
+    then:
+    - senseair.abc_disable:
+        id: senseair_s8
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Temperature in °F
+  id: display_in_f
+  restore_mode: RESTORE_DEFAULT_ON
+  optimistic: true
+  entity_category: config
+  icon: mdi:thermometer
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display AirGradient Default Page
+  id: display_ag_default_page
+  restore_mode: RESTORE_DEFAULT_ON
+  optimistic: true
+  entity_category: config
+  icon: mdi:monitor
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Summary Pages
+  id: display_summary_pages
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  entity_category: config
+  icon: mdi:monitor
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Air Quality Page
+  id: display_air_quality_page
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  entity_category: config
+  icon: mdi:monitor
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Temp/Hum Page
+  id: display_air_temp_page
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  entity_category: config
+  icon: mdi:monitor
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display VOC Page
+  id: display_voc_page
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  entity_category: config
+  icon: mdi:monitor
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Combo Page
+  id: display_combo_page
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  entity_category: config
+  icon: mdi:monitor
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Boot Page
+  id: display_boot_page
+  restore_mode: ALWAYS_ON
+  optimistic: true
+  entity_category: config
+  icon: mdi:monitor
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Blank Page
+  id: display_blank_page
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  entity_category: config
+  icon: mdi:monitor
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Upload to AirGradient Dashboard
+  id: upload_airgradient
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  disabled_by_default: false
+  assumed_state: false
+- platform: safe_mode
+  name: Flash Mode (Safe Mode)
+  icon: mdi:cellphone-arrow-down
+  disabled_by_default: false
+  restore_mode: ALWAYS_OFF
+  entity_category: config
+font:
+- file:
+    family: Open Sans
+    weight: 400
+    italic: false
+    type: gfonts
+  id: open_sans_14
+  size: 14
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+- file:
+    family: Open Sans
+    weight: 400
+    italic: false
+    type: gfonts
+  id: open_sans_9
+  size: 9
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+- file:
+    family: Open Sans
+    weight: 400
+    italic: false
+    type: gfonts
+  id: open_sans_20
+  size: 20
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+- file:
+    family: Poppins
+    weight: 300
+    italic: false
+    type: gfonts
+  id: poppins_light
+  size: 14
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+- file:
+    family: Poppins
+    weight: 300
+    italic: false
+    type: gfonts
+  id: poppins_light_12
+  size: 12
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+- file:
+    family: Ubuntu Mono
+    weight: 400
+    italic: false
+    type: gfonts
+  id: ubuntu
+  size: 22
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+display:
+- platform: ssd1306_i2c
+  model: SH1106_128X64
+  id: oled_display
+  address: 0x3C
+  pages:
+  - id: airgradient_default
+    lambda: !lambda |-
+      if (id(display_in_f).state) {
+        it.printf(0, 0, id(open_sans_14), "%.1f°F", id(temp).state*9/5+32);
+      } else {
+        it.printf(0, 0, id(open_sans_14), "%.1f°C", id(temp).state);
+      }
+      it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
+      it.line(0,17,128,17);
+      it.printf(0,19, id(open_sans_9), "CO2");
+      it.printf(0,27, id(open_sans_20), "%.0f", id(co2).state);
+      it.printf(0,52, id(open_sans_9), "ppm");
+      it.line(50,19,50,64);
+      it.printf(54, 19, id(open_sans_9), "PM2.5");
+      it.printf(54, 27, id(open_sans_20), "%.0f", id(pm_2_5).state);
+      it.printf(54, 52, id(open_sans_9), "µg/m³");
+      it.line(100,19,100,64);
+      it.printf(104,18, id(open_sans_9), "TVOC");
+      it.printf(104,29, id(open_sans_9), "%.0f", id(voc).state);
+      it.printf(104,41, id(open_sans_9), "NOx");
+      it.printf(104,52, id(open_sans_9), "%.0f", id(nox).state);
+  - id: boot
+    lambda: !lambda |-
+      it.printf(0, 0, id(open_sans_14), "ID:");
+      it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%s", get_mac_address().c_str());
+      it.printf(0, 21, id(open_sans_14), "Config Ver: 2.0.0");
+      it.printf(0, 42, id(open_sans_14), "AG One");
+  - id: summary1
+    lambda: !lambda |-
+      it.printf(0, 0, id(poppins_light), "CO2:");
+      it.printf(128, 0, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
+      it.printf(0, 16, id(poppins_light), "PM2.5:");
+      it.printf(128, 16, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f µg/m³", id(pm_2_5).state);
+      it.printf(0, 32, id(poppins_light), "Temp:");
+      if (id(display_in_f).state) {
+        it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f°F", id(temp).state*9/5+32);
+      } else {
+        it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f°C", id(temp).state);
+      }
+      it.printf(0, 48, id(poppins_light), "Humidity:");
+      it.printf(128, 48, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
+  - id: summary2
+    lambda: !lambda |-
+      it.printf(0, 0, id(poppins_light), "CO2:");
+      it.printf(128, 0, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
+      it.printf(0, 16, id(poppins_light), "PM2.5:");
+      it.printf(128, 16, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f µg/m³", id(pm_2_5).state);
+      it.printf(0, 32, id(poppins_light), "VOC:");
+      it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f", id(voc).state);
+      it.printf(0, 48, id(poppins_light), "NOx:");
+      it.printf(128, 48, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f", id(nox).state);
+  - id: air_quality
+    lambda: !lambda |-
+      it.printf(0, 6, id(ubuntu), "CO2");
+      it.printf(128, 6, id(ubuntu), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
+      it.printf(0, 34, id(ubuntu), "PM2");
+      it.printf(128, 34, id(ubuntu), TextAlign::TOP_RIGHT, "%.0f µg/m³", id(pm_2_5).state);
+  - id: air_temp
+    lambda: !lambda |-
+      it.printf(0, 6, id(ubuntu), "Temp");
+      if (id(display_in_f).state) {
+        it.printf(128, 6, id(ubuntu), TextAlign::TOP_RIGHT, "%.1f°F", id(temp).state*9/5+32);
+      } else {
+        it.printf(128, 6, id(ubuntu), TextAlign::TOP_RIGHT, "%.1f°C", id(temp).state);
+      }
+      it.printf(0, 34, id(ubuntu), "Humid");
+      it.printf(128, 34, id(ubuntu), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
+  - id: air_voc
+    lambda: !lambda |-
+      it.printf(0, 6, id(ubuntu), "VOC:");
+      it.printf(128, 6, id(ubuntu), TextAlign::TOP_RIGHT, "%.0f", id(voc).state);
+      it.printf(0, 34, id(ubuntu), "NOx:");
+      it.printf(128, 34, id(ubuntu), TextAlign::TOP_RIGHT, "%.0f", id(nox).state);
+  - id: combo
+    lambda: !lambda |-
+      if (id(display_in_f).state) {
+        it.printf(0, 0, id(poppins_light_12), "%.1f °F", id(temp).state*9/5+32);
+      } else {
+        it.printf(0, 0, id(poppins_light_12), "%.1f °C", id(temp).state);
+      }
+      it.printf(128, 0, id(poppins_light_12), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
+      it.printf(0, 16, id(poppins_light_12), "%.0f µg", id(pm_2_5).state);
+      it.printf(128, 16, id(poppins_light_12), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
+      it.printf(0, 32, id(poppins_light_12), "VOC: %.0f", id(voc).state);
+      it.printf(128, 32, id(poppins_light_12), TextAlign::TOP_RIGHT, "NOx: %.0f", id(nox).state);
+      it.printf(0, 48, id(poppins_light_12), "AQI: %.0f", id(pm_2_5_aqi).state);
+  - id: blank
+    lambda: !lambda |-
+      it.printf(0, 0, id(open_sans_14), "");
+  on_page_change:
+  - to: airgradient_default
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_ag_default_page
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  - to: summary1
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_summary_pages
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  - to: summary2
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_summary_pages
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  - to: air_quality
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_air_quality_page
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  - to: air_temp
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_air_temp_page
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  - to: air_voc
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_voc_page
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  - to: combo
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_combo_page
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  - to: boot
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_boot_page
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  - to: blank
+    then:
+    - if:
+        condition:
+          switch.is_off:
+            id: display_blank_page
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  auto_clear_enabled: true
+  brightness: 1.0
+  contrast: 1.0
+  flip_x: true
+  flip_y: true
+  offset_x: 0
+  offset_y: 0
+  invert: false
+  update_interval: 1s
+interval:
+- interval: 10s
+  startup_delay: 1s
+  then:
+  - if:
+      condition:
+        switch.is_on:
+          id: display_boot_page
+      then:
+      - switch.turn_off:
+          id: display_boot_page
+- interval: 5s
+  then:
+  - if:
+      condition:
+        switch.is_on:
+          id: display_boot_page
+      then:
+      - display.page.show:
+          id: boot
+      else:
+      - display.page.show_next:
+          id: oled_display
+      - component.update:
+          id: oled_display
+  startup_delay: 0s
+- interval: 1min
+  then:
+  - if:
+      condition:
+        light.is_on:
+          id: led_strip
+      then:
+      - if:
+          condition:
+            lambda: !lambda |-
+              return id(co2).state < 800;
+          then:
+          - light.turn_on:
+              id: led_strip
+              brightness: !lambda |-
+                return id(led_brightness).state / 100.0;
+              red: 0.0
+              green: 1.0
+              blue: 0.0
+              state: true
+      - if:
+          condition:
+            lambda: !lambda |-
+              return id(co2).state >= 800 && id(co2).state < 1000;
+          then:
+          - light.turn_on:
+              id: led_strip
+              brightness: !lambda |-
+                return id(led_brightness).state / 100.0;
+              red: 1.0
+              green: 1.0
+              blue: 0.0
+              state: true
+      - if:
+          condition:
+            lambda: !lambda |-
+              return id(co2).state >= 1000 && id(co2).state < 1500;
+          then:
+          - light.turn_on:
+              id: led_strip
+              brightness: !lambda |-
+                return id(led_brightness).state / 100.0;
+              red: 1.0
+              green: 0.7
+              blue: 0.0
+              state: true
+      - if:
+          condition:
+            lambda: !lambda |-
+              return id(co2).state >= 1500 && id(co2).state < 2000;
+          then:
+          - light.turn_on:
+              id: led_strip
+              brightness: !lambda |-
+                return id(led_brightness).state / 100.0;
+              red: 1.0
+              green: 0.0
+              blue: 0.0
+              state: true
+      - if:
+          condition:
+            lambda: !lambda |-
+              return id(co2).state >= 2000 && id(co2).state < 3000;
+          then:
+          - light.turn_on:
+              id: led_strip
+              brightness: !lambda |-
+                return id(led_brightness).state / 100.0;
+              red: 0.6
+              green: 0.0
+              blue: 0.6
+              state: true
+      - if:
+          condition:
+            lambda: !lambda |-
+              return id(co2).state >= 3000 && id(co2).state < 10000;
+          then:
+          - light.turn_on:
+              id: led_strip
+              brightness: !lambda |-
+                return id(led_brightness).state / 100.0;
+              red: 0.4
+              green: 0.0
+              blue: 0.0
+              state: true
+  startup_delay: 0s
+- interval: 150s
+  then:
+  - if:
+      condition:
+        switch.is_on:
+          id: upload_airgradient
+      then:
+      - http_request.post:
+          url: !lambda |-
+            return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures";
+          headers:
+            Content-Type: application/json
+          json:
+            wifi: !lambda |-
+              return to_string(id(wifi_dbm).state);
+            pm01: !lambda |-
+              return to_string(id(pm_1_0).state);
+            pm02: !lambda |-
+              return to_string(id(pm_2_5).state);
+            pm10: !lambda |-
+              return to_string(id(pm_10_0).state);
+            pm003_count: !lambda |-
+              return to_string(id(pm_0_3um).state);
+            rco2: !lambda |-
+              return to_string(id(co2).state);
+            atmp: !lambda |-
+              return to_string(id(temp).state);
+            rhum: !lambda |-
+              return to_string(id(humidity).state);
+            tvoc_index: !lambda |-
+              return to_string(id(voc).state);
+            nox_index: !lambda |-
+              return to_string(id(nox).state);
+          verify_ssl: true
+          method: POST
+  startup_delay: 0s
+- interval: 150s
+  then:
+  - output.turn_on:
+      id: watchdog
+  - delay: 20ms
+  - output.turn_off:
+      id: watchdog
+  startup_delay: 0s
+number:
+- platform: template
+  name: Display Contrast %
+  icon: mdi:lightbulb
+  id: display_contrast
+  min_value: 0.0
+  max_value: 100.0
+  step: 1.0
+  initial_value: 100.0
+  optimistic: true
+  restore_value: true
+  mode: SLIDER
+  on_value:
+  - then:
+    - lambda: !lambda |-
+        id(oled_display).set_contrast(id(display_contrast).state / 100.0);
+  disabled_by_default: false
+  update_interval: 60s
+- platform: template
+  name: LED Brightness %
+  icon: mdi:lightbulb
+  id: led_brightness
+  min_value: 0.0
+  max_value: 100.0
+  step: 1.0
+  initial_value: 100.0
+  optimistic: true
+  restore_value: true
+  mode: SLIDER
+  disabled_by_default: false
+  update_interval: 60s
+light:
+- platform: esp32_rmt_led_strip
+  rgb_order: GRB
+  pin: 10
+  num_leds: 11
+  rmt_channel: 0
+  chipset: WS2812
+  name: LED Strip
+  id: led_strip
+  restore_mode: RESTORE_DEFAULT_OFF
+  disabled_by_default: false
+  gamma_correct: 2.8
+  default_transition_length: 1s
+  flash_transition_length: 0s
+  is_rgbw: false
+http_request:
+  useragent: ESPHome
+  follow_redirects: true
+  redirect_limit: 3
+  timeout: 5s
+output:
+- platform: gpio
+  id: watchdog
+  pin:
+    number: 2
+    ignore_strapping_warning: true
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    drive_strength: 20.0
+binary_sensor:
+- platform: gpio
+  pin:
+    number: 9
+    mode:
+      input: true
+      pullup: true
+      output: false
+      open_drain: false
+      pulldown: false
+    inverted: true
+    ignore_strapping_warning: true
+    drive_strength: 20.0
+  internal: true
+  name: Configuration Button
+  id: config_button
+  on_multi_click:
+  - timing:
+    - state: true
+      min_length: 0ms
+      max_length: 1s
+    - state: false
+      min_length: 500ms
+    then:
+    - logger.log:
+        format: Toggling display betwen C and F
+        level: DEBUG
+        tag: main
+        args: []
+    - switch.toggle:
+        id: display_in_f
+    invalid_cooldown: 1s
+  - timing:
+    - state: true
+      min_length: 1s
+      max_length: 5s
+    - state: false
+      min_length: 500ms
+    then:
+    - logger.log:
+        format: Starting manual CO2 calibration
+        level: DEBUG
+        tag: main
+        args: []
+    - senseair.background_calibration:
+        id: senseair_s8
+    - delay: 70s
+    - senseair.background_calibration_result:
+        id: senseair_s8
+    invalid_cooldown: 1s
+  disabled_by_default: false
+api:
+  port: 6053
+  password: \033[5m''\033[6m
+  reboot_timeout: 15min
+ota:
+  safe_mode: true
+  port: 3232
+  reboot_timeout: 5min
+  num_attempts: 10
+wifi:
+  ap:
+    ap_timeout: 1min
+  domain: .local
+  reboot_timeout: 15min
+  power_save_mode: LIGHT
+  fast_connect: false
+  passive_scan: false
+  enable_on_boot: true
+  use_address: ag-one.local
+dashboard_import:
+  package_import_url: github://MallocArray/airgradient_esphome/airgradient-one.yaml
+  import_full_config: false
+
diff --git a/full_config/ag-open-air-o-1pst.yaml b/full_config/ag-open-air-o-1pst.yaml
index 2c156ff..c655761 100644
--- a/full_config/ag-open-air-o-1pst.yaml
+++ b/full_config/ag-open-air-o-1pst.yaml
@@ -1,444 +1,457 @@
-substitutions:
-  devicename: ag-open-air-o-1pst
-  friendly_devicename: AG Open Air O-1PST
-  ag_esphome_config_version: 1.0.0
-  add_mac_suffix: 'true'
-esphome:
-  name: ag-open-air-o-1pst
-  friendly_name: AG Open Air O-1PST
-  name_add_mac_suffix: true
-  project:
-    name: mallocarray.airgradient
-    version: 1.0.0
-  min_version: 2023.7.0
-  on_boot:
-  - priority: 200.0
-    then:
-    - if:
-        condition:
-          switch.is_on:
-            id: upload_airgradient
-        then:
-        - http_request.post:
-            url: !lambda |-
-              return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures";
-            headers:
-              Content-Type: application/json
-            json:
-              wifi: !lambda |-
-                return to_string(-50);
-            verify_ssl: true
-            method: POST
-  build_path: build/ag-open-air-o-1pst
-  area: ''
-  platformio_options: {}
-  includes: []
-  libraries: []
-esp32:
-  board: esp32-c3-devkitm-1
-  flash_size: 4MB
-  framework:
-    version: 2.0.5
-    source: ~3.20005.0
-    platform_version: platformio/espressif32@5.4.0
-    type: arduino
-  variant: ESP32C3
-logger:
-  baud_rate: 0
-  logs:
-    component: ERROR
-  tx_buffer_size: 512
-  deassert_rts_dtr: false
-  hardware_uart: UART0
-  level: DEBUG
-captive_portal: {}
-uart:
-- rx_pin:
-    number: 0
-    mode:
-      input: true
-      output: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    ignore_strapping_warning: false
-    drive_strength: 20.0
-  tx_pin:
-    number: 1
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    ignore_strapping_warning: false
-    drive_strength: 20.0
-  baud_rate: 9600
-  id: senseair_s8_uart
-  rx_buffer_size: 256
-  stop_bits: 1
-  data_bits: 8
-  parity: NONE
-- rx_pin:
-    number: 20
-    mode:
-      input: true
-      output: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    ignore_strapping_warning: false
-    drive_strength: 20.0
-  tx_pin:
-    number: 21
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    ignore_strapping_warning: false
-    drive_strength: 20.0
-  baud_rate: 9600
-  id: pms5003_uart
-  rx_buffer_size: 256
-  stop_bits: 1
-  data_bits: 8
-  parity: NONE
-i2c:
-- sda: 7
-  scl: 6
-  frequency: 400000.0
-  scan: true
-sensor:
-- platform: pmsx003
-  type: PMS5003T
-  uart_id: pms5003_uart
-  pm_2_5:
-    name: PM 2.5
-    id: pm_2_5
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_1_0:
-    name: PM 1.0
-    id: pm_1_0
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_10_0:
-    name: PM 10.0
-    id: pm_10_0
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_0_3um:
-    name: PM 0.3
-    id: pm_0_3um
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: /dL
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-  temperature:
-    name: Temperature
-    id: temp
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: °C
-    accuracy_decimals: 1
-    device_class: temperature
-    state_class: measurement
-  humidity:
-    name: Humidity
-    id: humidity
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: '%'
-    accuracy_decimals: 1
-    device_class: humidity
-    state_class: measurement
-  update_interval: 0s
-- platform: template
-  name: PM 2.5 AQI
-  id: pm_2_5_aqi
-  update_interval: 5min
-  unit_of_measurement: AQI
-  icon: mdi:air-filter
-  accuracy_decimals: 0
-  filters:
-  - skip_initial: 10
-  lambda: !lambda |-
-    // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
-    // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
-    if (id(pm_2_5).state <= 12.0) {
-    // good
-    return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
-    } else if (id(pm_2_5).state <= 35.4) {
-    // moderate
-    return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
-    } else if (id(pm_2_5).state <= 55.4) {
-    // usg
-    return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
-    } else if (id(pm_2_5).state <= 150.4) {
-    // unhealthy
-    return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
-    } else if (id(pm_2_5).state <= 250.4) {
-    // very unhealthy
-    return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
-    } else if (id(pm_2_5).state <= 350.4) {
-    // hazardous
-    return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
-    } else if (id(pm_2_5).state <= 500.4) {
-    // hazardous 2
-    return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
-    } else {
-    return(500);
-    }
-  disabled_by_default: false
-  force_update: false
-- platform: senseair
-  co2:
-    name: CO2
-    id: co2
-    filters:
-    - skip_initial: 2
-    - clamp:
-        min_value: 400.0
-        max_value: .nan
-        ignore_out_of_range: false
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: ppm
-    icon: mdi:molecule-co2
-    accuracy_decimals: 0
-    device_class: carbon_dioxide
-    state_class: measurement
-  id: senseair_s8
-  uart_id: senseair_s8_uart
-  update_interval: 60s
-- platform: sgp4x
-  voc:
-    name: VOC Index
-    id: voc
-    disabled_by_default: false
-    force_update: false
-    icon: mdi:radiator
-    accuracy_decimals: 0
-    device_class: aqi
-    state_class: measurement
-  nox:
-    name: NOx Index
-    id: nox
-    disabled_by_default: false
-    force_update: false
-    icon: mdi:radiator
-    accuracy_decimals: 0
-    device_class: aqi
-    state_class: measurement
-  compensation:
-    temperature_source: temp
-    humidity_source: humidity
-  store_baseline: true
-  update_interval: 60s
-  address: 0x59
-- platform: wifi_signal
-  name: WiFi Signal
-  id: wifi_dbm
-  update_interval: 60s
-  disabled_by_default: false
-  force_update: false
-  unit_of_measurement: dBm
-  accuracy_decimals: 0
-  device_class: signal_strength
-  state_class: measurement
-  entity_category: diagnostic
-- platform: uptime
-  name: Uptime
-  id: device_uptime
-  disabled_by_default: false
-  force_update: false
-  unit_of_measurement: s
-  icon: mdi:timer-outline
-  accuracy_decimals: 0
-  device_class: duration
-  state_class: total_increasing
-  entity_category: diagnostic
-  update_interval: 60s
-button:
-- platform: template
-  name: SenseAir S8 Calibration
-  id: senseair_s8_calibrate_button
-  on_press:
-  - then:
-    - senseair.background_calibration:
-        id: senseair_s8
-    - delay: 70s
-    - senseair.background_calibration_result:
-        id: senseair_s8
-  disabled_by_default: false
-- platform: template
-  name: SenseAir S8 Show Calibration Interval
-  id: senseair_s8_show_calibrate_interval
-  on_press:
-  - then:
-    - senseair.abc_get_period:
-        id: senseair_s8
-  disabled_by_default: false
-switch:
-- platform: template
-  name: SenseAir S8 Automatic Background Calibration
-  id: senseair_s8_abc_switch
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  entity_category: config
-  turn_on_action:
-    then:
-    - senseair.abc_enable:
-        id: senseair_s8
-  turn_off_action:
-    then:
-    - senseair.abc_disable:
-        id: senseair_s8
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Upload to AirGradient Dashboard
-  id: upload_airgradient
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  disabled_by_default: false
-  assumed_state: false
-- platform: safe_mode
-  name: Flash Mode (Safe Mode)
-  icon: mdi:cellphone-arrow-down
-  disabled_by_default: false
-  restore_mode: ALWAYS_OFF
-  entity_category: config
-interval:
-- interval: 150s
-  then:
-  - if:
-      condition:
-        switch.is_on:
-          id: upload_airgradient
-      then:
-      - http_request.post:
-          url: !lambda |-
-            return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures";
-          headers:
-            Content-Type: application/json
-          json:
-            wifi: !lambda |-
-              return to_string(id(wifi_dbm).state);
-            pm01: !lambda |-
-              return to_string(id(pm_1_0).state);
-            pm02: !lambda |-
-              return to_string(id(pm_2_5).state);
-            pm10: !lambda |-
-              return to_string(id(pm_10_0).state);
-            pm003_count: !lambda |-
-              return to_string(id(pm_0_3um).state);
-            rco2: !lambda |-
-              return to_string(id(co2).state);
-            atmp: !lambda |-
-              return to_string(id(temp).state);
-            rhum: !lambda |-
-              return to_string(id(humidity).state);
-            tvoc_index: !lambda |-
-              return to_string(id(voc).state);
-            nox_index: !lambda |-
-              return to_string(id(nox).state);
-          verify_ssl: true
-          method: POST
-  startup_delay: 0s
-- interval: 150s
-  then:
-  - output.turn_on:
-      id: watchdog
-  - delay: 20ms
-  - output.turn_off:
-      id: watchdog
-  startup_delay: 0s
-http_request:
-  useragent: ESPHome
-  follow_redirects: true
-  redirect_limit: 3
-  timeout: 5s
-output:
-- platform: gpio
-  id: watchdog
-  pin:
-    number: 2
-    ignore_strapping_warning: true
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-    inverted: false
-    drive_strength: 20.0
-api:
-  port: 6053
-  password: \033[5m''\033[6m
-  reboot_timeout: 15min
-ota:
-  safe_mode: true
-  port: 3232
-  reboot_timeout: 5min
-  num_attempts: 10
-wifi:
-  ap:
-    ap_timeout: 1min
-  domain: .local
-  reboot_timeout: 15min
-  power_save_mode: LIGHT
-  fast_connect: false
-  passive_scan: false
-  enable_on_boot: true
-  use_address: ag-open-air-o-1pst.local
-
+substitutions:
+  name: ag-open-air-o-1pst
+  friendly_name: AG Open Air O-1PST
+  config_version: 2.0.0
+  name_add_mac_suffix: 'false'
+esphome:
+  name: ag-open-air-o-1pst
+  friendly_name: AG Open Air O-1PST
+  name_add_mac_suffix: false
+  project:
+    name: mallocarray.airgradient
+    version: 2.0.0
+  min_version: 2023.12.0
+  on_boot:
+  - priority: 200.0
+    then:
+    - if:
+        condition:
+          switch.is_on:
+            id: upload_airgradient
+        then:
+        - http_request.post:
+            url: !lambda |-
+              return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures";
+            headers:
+              Content-Type: application/json
+            json:
+              wifi: !lambda |-
+                return to_string(-50);
+            verify_ssl: true
+            method: POST
+  build_path: build/ag-open-air-o-1pst
+  area: ''
+  platformio_options: {}
+  includes: []
+  libraries: []
+esp32:
+  board: esp32-c3-devkitm-1
+  flash_size: 4MB
+  framework:
+    version: 2.0.5
+    source: ~3.20005.0
+    platform_version: platformio/espressif32@5.4.0
+    type: arduino
+  variant: ESP32C3
+logger:
+  baud_rate: 0
+  logs:
+    component: ERROR
+  tx_buffer_size: 512
+  deassert_rts_dtr: false
+  hardware_uart: UART0
+  level: DEBUG
+captive_portal: {}
+uart:
+- rx_pin:
+    number: 0
+    mode:
+      input: true
+      output: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    ignore_strapping_warning: false
+    drive_strength: 20.0
+  tx_pin:
+    number: 1
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    ignore_strapping_warning: false
+    drive_strength: 20.0
+  baud_rate: 9600
+  id: senseair_s8_uart
+  rx_buffer_size: 256
+  stop_bits: 1
+  data_bits: 8
+  parity: NONE
+- rx_pin:
+    number: 20
+    mode:
+      input: true
+      output: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    ignore_strapping_warning: false
+    drive_strength: 20.0
+  tx_pin:
+    number: 21
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    ignore_strapping_warning: false
+    drive_strength: 20.0
+  baud_rate: 9600
+  id: pms5003_uart
+  rx_buffer_size: 256
+  stop_bits: 1
+  data_bits: 8
+  parity: NONE
+i2c:
+- sda: 7
+  scl: 6
+  frequency: 400000.0
+  scan: true
+button:
+- platform: factory_reset
+  disabled_by_default: true
+  name: Factory Reset ESP
+  id: factory_reset_all
+  icon: mdi:restart-alert
+  entity_category: config
+  device_class: restart
+- platform: template
+  name: SenseAir S8 Calibration
+  id: senseair_s8_calibrate_button
+  on_press:
+  - then:
+    - senseair.background_calibration:
+        id: senseair_s8
+    - delay: 70s
+    - senseair.background_calibration_result:
+        id: senseair_s8
+  disabled_by_default: false
+- platform: template
+  name: SenseAir S8 Show Calibration Interval
+  id: senseair_s8_show_calibrate_interval
+  on_press:
+  - then:
+    - senseair.abc_get_period:
+        id: senseair_s8
+  disabled_by_default: false
+sensor:
+- platform: pmsx003
+  type: PMS5003T
+  uart_id: pms5003_uart
+  pm_2_5:
+    name: PM 2.5
+    id: pm_2_5
+    device_class: pm25
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_1_0:
+    name: PM 1.0
+    id: pm_1_0
+    device_class: pm1
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_10_0:
+    name: PM 10.0
+    id: pm_10_0
+    device_class: pm10
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_0_3um:
+    name: PM 0.3
+    id: pm_0_3um
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: /dL
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+  temperature:
+    name: Temperature
+    id: temp
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: °C
+    accuracy_decimals: 1
+    device_class: temperature
+    state_class: measurement
+  humidity:
+    name: Humidity
+    id: humidity
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: '%'
+    accuracy_decimals: 1
+    device_class: humidity
+    state_class: measurement
+  update_interval: 0s
+- platform: template
+  name: PM 2.5 AQI
+  id: pm_2_5_aqi
+  update_interval: 5min
+  unit_of_measurement: AQI
+  icon: mdi:air-filter
+  accuracy_decimals: 0
+  filters:
+  - skip_initial: 10
+  lambda: !lambda |-
+    // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
+    // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
+    if (id(pm_2_5).state <= 12.0) {
+    // good
+    return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
+    } else if (id(pm_2_5).state <= 35.4) {
+    // moderate
+    return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
+    } else if (id(pm_2_5).state <= 55.4) {
+    // usg
+    return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
+    } else if (id(pm_2_5).state <= 150.4) {
+    // unhealthy
+    return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
+    } else if (id(pm_2_5).state <= 250.4) {
+    // very unhealthy
+    return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
+    } else if (id(pm_2_5).state <= 350.4) {
+    // hazardous
+    return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
+    } else if (id(pm_2_5).state <= 500.4) {
+    // hazardous 2
+    return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
+    } else {
+    return(500);
+    }
+  disabled_by_default: false
+  force_update: false
+- platform: senseair
+  co2:
+    name: CO2
+    id: co2
+    filters:
+    - skip_initial: 2
+    - clamp:
+        min_value: 400.0
+        max_value: .nan
+        ignore_out_of_range: false
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: ppm
+    icon: mdi:molecule-co2
+    accuracy_decimals: 0
+    device_class: carbon_dioxide
+    state_class: measurement
+  id: senseair_s8
+  uart_id: senseair_s8_uart
+  update_interval: 60s
+- platform: sgp4x
+  voc:
+    name: VOC Index
+    id: voc
+    disabled_by_default: false
+    force_update: false
+    icon: mdi:radiator
+    accuracy_decimals: 0
+    device_class: aqi
+    state_class: measurement
+  nox:
+    name: NOx Index
+    id: nox
+    disabled_by_default: false
+    force_update: false
+    icon: mdi:radiator
+    accuracy_decimals: 0
+    device_class: aqi
+    state_class: measurement
+  compensation:
+    temperature_source: temp
+    humidity_source: humidity
+  store_baseline: true
+  update_interval: 60s
+  address: 0x59
+- platform: wifi_signal
+  name: WiFi Signal
+  id: wifi_dbm
+  update_interval: 60s
+  disabled_by_default: false
+  force_update: false
+  unit_of_measurement: dBm
+  accuracy_decimals: 0
+  device_class: signal_strength
+  state_class: measurement
+  entity_category: diagnostic
+- platform: uptime
+  name: Uptime
+  id: device_uptime
+  disabled_by_default: false
+  force_update: false
+  unit_of_measurement: s
+  icon: mdi:timer-outline
+  accuracy_decimals: 0
+  device_class: duration
+  state_class: total_increasing
+  entity_category: diagnostic
+  update_interval: 60s
+switch:
+- platform: template
+  name: SenseAir S8 Automatic Background Calibration
+  id: senseair_s8_abc_switch
+  restore_mode: RESTORE_DEFAULT_ON
+  optimistic: true
+  entity_category: config
+  turn_on_action:
+    then:
+    - senseair.abc_enable:
+        id: senseair_s8
+  turn_off_action:
+    then:
+    - senseair.abc_disable:
+        id: senseair_s8
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Upload to AirGradient Dashboard
+  id: upload_airgradient
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  disabled_by_default: false
+  assumed_state: false
+- platform: safe_mode
+  name: Flash Mode (Safe Mode)
+  icon: mdi:cellphone-arrow-down
+  disabled_by_default: false
+  restore_mode: ALWAYS_OFF
+  entity_category: config
+interval:
+- interval: 150s
+  then:
+  - if:
+      condition:
+        switch.is_on:
+          id: upload_airgradient
+      then:
+      - http_request.post:
+          url: !lambda |-
+            return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures";
+          headers:
+            Content-Type: application/json
+          json:
+            wifi: !lambda |-
+              return to_string(id(wifi_dbm).state);
+            pm01: !lambda |-
+              return to_string(id(pm_1_0).state);
+            pm02: !lambda |-
+              return to_string(id(pm_2_5).state);
+            pm10: !lambda |-
+              return to_string(id(pm_10_0).state);
+            pm003_count: !lambda |-
+              return to_string(id(pm_0_3um).state);
+            rco2: !lambda |-
+              return to_string(id(co2).state);
+            atmp: !lambda |-
+              return to_string(id(temp).state);
+            rhum: !lambda |-
+              return to_string(id(humidity).state);
+            tvoc_index: !lambda |-
+              return to_string(id(voc).state);
+            nox_index: !lambda |-
+              return to_string(id(nox).state);
+          verify_ssl: true
+          method: POST
+  startup_delay: 0s
+- interval: 150s
+  then:
+  - output.turn_on:
+      id: watchdog
+  - delay: 20ms
+  - output.turn_off:
+      id: watchdog
+  startup_delay: 0s
+http_request:
+  useragent: ESPHome
+  follow_redirects: true
+  redirect_limit: 3
+  timeout: 5s
+output:
+- platform: gpio
+  id: watchdog
+  pin:
+    number: 2
+    ignore_strapping_warning: true
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+    inverted: false
+    drive_strength: 20.0
+api:
+  port: 6053
+  password: \033[5m''\033[6m
+  reboot_timeout: 15min
+ota:
+  safe_mode: true
+  port: 3232
+  reboot_timeout: 5min
+  num_attempts: 10
+wifi:
+  ap:
+    ap_timeout: 1min
+  domain: .local
+  reboot_timeout: 15min
+  power_save_mode: LIGHT
+  fast_connect: false
+  passive_scan: false
+  enable_on_boot: true
+  use_address: ag-open-air-o-1pst.local
+dashboard_import:
+  package_import_url: github://MallocArray/airgradient_esphome/airgradient-open-air-o-1pst.yaml
+  import_full_config: false
+
diff --git a/full_config/ag-pro.yaml b/full_config/ag-pro.yaml
index c472d3e..b84b833 100644
--- a/full_config/ag-pro.yaml
+++ b/full_config/ag-pro.yaml
@@ -1,809 +1,840 @@
-substitutions:
-  devicename: ag-pro
-  friendly_devicename: AG Pro
-  ag_esphome_config_version: 1.0.0
-  add_mac_suffix: 'true'
-esphome:
-  name: ag-pro
-  friendly_name: AG Pro
-  name_add_mac_suffix: true
-  project:
-    name: mallocarray.airgradient
-    version: 1.0.0
-  min_version: 2023.7.0
-  on_boot:
-  - priority: 200.0
-    then:
-    - if:
-        condition:
-          switch.is_on:
-            id: upload_airgradient
-        then:
-        - http_request.post:
-            url: !lambda |-
-              return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
-            headers:
-              Content-Type: application/json
-            json:
-              wifi: !lambda |-
-                return to_string(-50);
-            verify_ssl: true
-            method: POST
-  build_path: build/ag-pro
-  area: ''
-  platformio_options: {}
-  includes: []
-  libraries: []
-esp8266:
-  board: d1_mini
-  restore_from_flash: true
-  framework:
-    version: 3.0.2
-    source: ~3.30002.0
-    platform_version: platformio/espressif8266@3.2.0
-  early_pin_init: true
-  board_flash_mode: dout
-logger:
-  logs:
-    component: ERROR
-  baud_rate: 115200
-  tx_buffer_size: 512
-  deassert_rts_dtr: false
-  hardware_uart: UART0
-  level: DEBUG
-  esp8266_store_log_strings_in_flash: true
-captive_portal: {}
-uart:
-- rx_pin:
-    number: 2
-    mode:
-      input: true
-      output: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  tx_pin:
-    number: 0
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  baud_rate: 9600
-  id: senseair_s8_uart
-  rx_buffer_size: 256
-  stop_bits: 1
-  data_bits: 8
-  parity: NONE
-- rx_pin:
-    number: 14
-    mode:
-      input: true
-      output: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  tx_pin:
-    number: 12
-    mode:
-      output: true
-      input: false
-      open_drain: false
-      pullup: false
-      pulldown: false
-      analog: false
-    inverted: false
-  baud_rate: 9600
-  id: pms5003_uart
-  rx_buffer_size: 256
-  stop_bits: 1
-  data_bits: 8
-  parity: NONE
-i2c:
-- sda: 4
-  scl: 5
-  frequency: 400000.0
-  scan: true
-sensor:
-- platform: pmsx003
-  type: PMSX003
-  uart_id: pms5003_uart
-  pm_2_5:
-    name: PM 2.5
-    id: pm_2_5
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_1_0:
-    name: PM 1.0
-    id: pm_1_0
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_10_0:
-    name: PM 10.0
-    id: pm_10_0
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: µg/m³
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-    state_class: measurement
-  pm_0_3um:
-    name: PM 0.3
-    id: pm_0_3um
-    filters:
-    - sliding_window_moving_average:
-        window_size: 30
-        send_every: 30
-        send_first_at: 1
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: /dL
-    icon: mdi:chemical-weapon
-    accuracy_decimals: 0
-  update_interval: 0s
-- platform: template
-  name: PM 2.5 AQI
-  id: pm_2_5_aqi
-  update_interval: 5min
-  unit_of_measurement: AQI
-  icon: mdi:air-filter
-  accuracy_decimals: 0
-  filters:
-  - skip_initial: 10
-  lambda: !lambda |-
-    // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
-    // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
-    if (id(pm_2_5).state <= 12.0) {
-    // good
-    return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
-    } else if (id(pm_2_5).state <= 35.4) {
-    // moderate
-    return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
-    } else if (id(pm_2_5).state <= 55.4) {
-    // usg
-    return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
-    } else if (id(pm_2_5).state <= 150.4) {
-    // unhealthy
-    return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
-    } else if (id(pm_2_5).state <= 250.4) {
-    // very unhealthy
-    return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
-    } else if (id(pm_2_5).state <= 350.4) {
-    // hazardous
-    return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
-    } else if (id(pm_2_5).state <= 500.4) {
-    // hazardous 2
-    return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
-    } else {
-    return(500);
-    }
-  disabled_by_default: false
-  force_update: false
-- platform: senseair
-  co2:
-    name: CO2
-    id: co2
-    filters:
-    - skip_initial: 2
-    - clamp:
-        min_value: 400.0
-        max_value: .nan
-        ignore_out_of_range: false
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: ppm
-    icon: mdi:molecule-co2
-    accuracy_decimals: 0
-    device_class: carbon_dioxide
-    state_class: measurement
-  id: senseair_s8
-  uart_id: senseair_s8_uart
-  update_interval: 60s
-- platform: sht4x
-  temperature:
-    name: Temperature
-    id: temp
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: °C
-    icon: mdi:thermometer
-    accuracy_decimals: 2
-    device_class: temperature
-    state_class: measurement
-  humidity:
-    name: Humidity
-    id: humidity
-    disabled_by_default: false
-    force_update: false
-    unit_of_measurement: '%'
-    icon: mdi:water-percent
-    accuracy_decimals: 2
-    device_class: humidity
-    state_class: measurement
-  address: 0x44
-  precision: High
-  heater_power: High
-  heater_time: Long
-  heater_max_duty: 0.0
-  update_interval: 60s
-- platform: sgp4x
-  voc:
-    name: VOC Index
-    id: voc
-    disabled_by_default: false
-    force_update: false
-    icon: mdi:radiator
-    accuracy_decimals: 0
-    device_class: aqi
-    state_class: measurement
-  nox:
-    name: NOx Index
-    id: nox
-    disabled_by_default: false
-    force_update: false
-    icon: mdi:radiator
-    accuracy_decimals: 0
-    device_class: aqi
-    state_class: measurement
-  compensation:
-    temperature_source: temp
-    humidity_source: humidity
-  store_baseline: true
-  update_interval: 60s
-  address: 0x59
-- platform: wifi_signal
-  name: WiFi Signal
-  id: wifi_dbm
-  update_interval: 60s
-  disabled_by_default: false
-  force_update: false
-  unit_of_measurement: dBm
-  accuracy_decimals: 0
-  device_class: signal_strength
-  state_class: measurement
-  entity_category: diagnostic
-- platform: uptime
-  name: Uptime
-  id: device_uptime
-  disabled_by_default: false
-  force_update: false
-  unit_of_measurement: s
-  icon: mdi:timer-outline
-  accuracy_decimals: 0
-  device_class: duration
-  state_class: total_increasing
-  entity_category: diagnostic
-  update_interval: 60s
-button:
-- platform: template
-  name: SenseAir S8 Calibration
-  id: senseair_s8_calibrate_button
-  on_press:
-  - then:
-    - senseair.background_calibration:
-        id: senseair_s8
-    - delay: 70s
-    - senseair.background_calibration_result:
-        id: senseair_s8
-  disabled_by_default: false
-- platform: template
-  name: SenseAir S8 Show Calibration Interval
-  id: senseair_s8_show_calibrate_interval
-  on_press:
-  - then:
-    - senseair.abc_get_period:
-        id: senseair_s8
-  disabled_by_default: false
-switch:
-- platform: template
-  name: SenseAir S8 Automatic Background Calibration
-  id: senseair_s8_abc_switch
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  entity_category: config
-  turn_on_action:
-    then:
-    - senseair.abc_enable:
-        id: senseair_s8
-  turn_off_action:
-    then:
-    - senseair.abc_disable:
-        id: senseair_s8
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Display Temperature in °F
-  icon: mdi:thermometer
-  id: display_in_f
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  disabled_by_default: false
-  assumed_state: false
-- platform: template
-  name: Upload to AirGradient Dashboard
-  id: upload_airgradient
-  restore_mode: RESTORE_DEFAULT_ON
-  optimistic: true
-  disabled_by_default: false
-  assumed_state: false
-- platform: safe_mode
-  name: Flash Mode (Safe Mode)
-  icon: mdi:cellphone-arrow-down
-  disabled_by_default: false
-  restore_mode: ALWAYS_OFF
-  entity_category: config
-font:
-- file:
-    family: Open Sans
-    weight: 400
-    italic: false
-    type: gfonts
-  id: open_sans_14
-  size: 14
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-- file:
-    family: Open Sans
-    weight: 400
-    italic: false
-    type: gfonts
-  id: open_sans_9
-  size: 9
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-- file:
-    family: Open Sans
-    weight: 400
-    italic: false
-    type: gfonts
-  id: open_sans_20
-  size: 20
-  glyphs:
-  - ' '
-  - '!'
-  - '"'
-  - '%'
-  - (
-  - )
-  - +
-  - ','
-  - '-'
-  - .
-  - /
-  - '0'
-  - '1'
-  - '2'
-  - '3'
-  - '4'
-  - '5'
-  - '6'
-  - '7'
-  - '8'
-  - '9'
-  - ':'
-  - '='
-  - A
-  - B
-  - C
-  - D
-  - E
-  - F
-  - G
-  - H
-  - I
-  - J
-  - K
-  - L
-  - M
-  - N
-  - O
-  - P
-  - Q
-  - R
-  - S
-  - T
-  - U
-  - V
-  - W
-  - X
-  - Y
-  - Z
-  - _
-  - a
-  - b
-  - c
-  - d
-  - e
-  - f
-  - g
-  - h
-  - i
-  - j
-  - k
-  - l
-  - m
-  - n
-  - o
-  - p
-  - q
-  - r
-  - s
-  - t
-  - u
-  - v
-  - w
-  - x
-  - y
-  - z
-  - °
-  - ³
-  - µ
-display:
-- platform: ssd1306_i2c
-  model: SH1106_128X64
-  id: oled_display
-  address: 0x3C
-  pages:
-  - id: summary
-    lambda: !lambda |-
-      if (id(display_in_f).state) {
-        it.printf(0, 0, id(open_sans_14), "%.1f°F", id(temp).state*9/5+32);
-      } else {
-        it.printf(0, 0, id(open_sans_14), "%.1f°C", id(temp).state);
-      }
-      it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
-      it.line(0,17,128,17);
-      it.printf(0,19, id(open_sans_9), "CO2");
-      it.printf(0,27, id(open_sans_20), "%.0f", id(co2).state);
-      it.printf(0,52, id(open_sans_9), "ppm");
-      it.line(50,19,50,64);
-      it.printf(54, 19, id(open_sans_9), "PM2.5");
-      it.printf(54, 27, id(open_sans_20), "%.0f", id(pm_2_5).state);
-      it.printf(54, 52, id(open_sans_9), "µg/m³");
-      it.line(100,19,100,64);
-      it.printf(104,18, id(open_sans_9), "TVOC");
-      it.printf(104,29, id(open_sans_9), "%.0f", id(voc).state);
-      it.printf(104,41, id(open_sans_9), "NOx");
-      it.printf(104,52, id(open_sans_9), "%.0f", id(nox).state);
-  - id: boot
-    lambda: !lambda |-
-      it.printf(0, 0, id(open_sans_14), "ID:");
-      it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%s", get_mac_address().c_str());
-      it.printf(0, 21, id(open_sans_14), "Config Ver: 1.0.0");
-      it.printf(0, 42, id(open_sans_14), "AG Pro");
-  on_page_change:
-  - to: boot
-    then:
-    - if:
-        condition:
-          lambda: !lambda |-
-            return id(device_uptime).state > 30;
-        then:
-        - display.page.show_next:
-            id: oled_display
-        - component.update:
-            id: oled_display
-  auto_clear_enabled: true
-  brightness: 1.0
-  contrast: 1.0
-  flip_x: true
-  flip_y: true
-  offset_x: 0
-  offset_y: 0
-  invert: false
-  update_interval: 1s
-interval:
-- interval: 5s
-  then:
-  - if:
-      condition:
-        lambda: !lambda |-
-          return id(device_uptime).state < 10;
-      then:
-      - display.page.show:
-          id: boot
-      else:
-      - display.page.show_next:
-          id: oled_display
-      - component.update:
-          id: oled_display
-  startup_delay: 0s
-- interval: 150s
-  then:
-  - if:
-      condition:
-        switch.is_on:
-          id: upload_airgradient
-      then:
-      - http_request.post:
-          url: !lambda |-
-            return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
-          headers:
-            Content-Type: application/json
-          json:
-            wifi: !lambda |-
-              return to_string(id(wifi_dbm).state);
-            pm01: !lambda |-
-              return to_string(id(pm_1_0).state);
-            pm02: !lambda |-
-              return to_string(id(pm_2_5).state);
-            pm10: !lambda |-
-              return to_string(id(pm_10_0).state);
-            pm003_count: !lambda |-
-              return to_string(id(pm_0_3um).state);
-            rco2: !lambda |-
-              return to_string(id(co2).state);
-            atmp: !lambda |-
-              return to_string(id(temp).state);
-            rhum: !lambda |-
-              return to_string(id(humidity).state);
-            tvoc_index: !lambda |-
-              return to_string(id(voc).state);
-            nox_index: !lambda |-
-              return to_string(id(nox).state);
-          verify_ssl: true
-          method: POST
-  startup_delay: 0s
-http_request:
-  useragent: ESPHome
-  follow_redirects: true
-  redirect_limit: 3
-  timeout: 5s
-  esp8266_disable_ssl_support: false
-binary_sensor:
-- platform: gpio
-  pin:
-    number: 13
-    mode:
-      input: true
-      pullup: true
-      output: false
-      open_drain: false
-      pulldown: false
-      analog: false
-    inverted: true
-  internal: true
-  name: Configuration Button
-  id: config_button
-  on_multi_click:
-  - timing:
-    - state: true
-      min_length: 0ms
-      max_length: 1s
-    - state: false
-      min_length: 500ms
-    then:
-    - logger.log:
-        format: Toggling display betwen C and F
-        args: []
-        tag: main
-        level: DEBUG
-    - switch.toggle:
-        id: display_in_f
-    invalid_cooldown: 1s
-  - timing:
-    - state: true
-      min_length: 1s
-      max_length: 5s
-    - state: false
-      min_length: 500ms
-    then:
-    - logger.log:
-        format: Starting manual CO2 calibration
-        args: []
-        tag: main
-        level: DEBUG
-    - senseair.background_calibration:
-        id: senseair_s8
-    - delay: 70s
-    - senseair.background_calibration_result:
-        id: senseair_s8
-    invalid_cooldown: 1s
-  disabled_by_default: false
-api:
-  port: 6053
-  password: \033[5m''\033[6m
-  reboot_timeout: 15min
-ota:
-  safe_mode: true
-  port: 8266
-  reboot_timeout: 5min
-  num_attempts: 10
-wifi:
-  ap:
-    ap_timeout: 1min
-  domain: .local
-  reboot_timeout: 15min
-  power_save_mode: NONE
-  fast_connect: false
-  output_power: 20.0
-  passive_scan: false
-  enable_on_boot: true
-  use_address: ag-pro.local
-
+substitutions:
+  name: ag-pro
+  friendly_name: AG Pro
+  config_version: 2.0.0
+  name_add_mac_suffix: 'false'
+esphome:
+  name: ag-pro
+  friendly_name: AG Pro
+  name_add_mac_suffix: false
+  project:
+    name: mallocarray.airgradient
+    version: 2.0.0
+  min_version: 2023.12.0
+  on_boot:
+  - priority: 200.0
+    then:
+    - if:
+        condition:
+          switch.is_on:
+            id: upload_airgradient
+        then:
+        - http_request.post:
+            url: !lambda |-
+              return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
+            headers:
+              Content-Type: application/json
+            json:
+              wifi: !lambda |-
+                return to_string(-50);
+            verify_ssl: true
+            method: POST
+  build_path: build/ag-pro
+  area: ''
+  platformio_options: {}
+  includes: []
+  libraries: []
+esp8266:
+  board: d1_mini
+  restore_from_flash: true
+  framework:
+    version: 3.0.2
+    source: ~3.30002.0
+    platform_version: platformio/espressif8266@3.2.0
+  early_pin_init: true
+  board_flash_mode: dout
+logger:
+  logs:
+    component: ERROR
+  baud_rate: 115200
+  tx_buffer_size: 512
+  deassert_rts_dtr: false
+  hardware_uart: UART0
+  level: DEBUG
+  esp8266_store_log_strings_in_flash: true
+captive_portal: {}
+uart:
+- rx_pin:
+    number: 2
+    mode:
+      input: true
+      output: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  tx_pin:
+    number: 0
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  baud_rate: 9600
+  id: senseair_s8_uart
+  rx_buffer_size: 256
+  stop_bits: 1
+  data_bits: 8
+  parity: NONE
+- rx_pin:
+    number: 14
+    mode:
+      input: true
+      output: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  tx_pin:
+    number: 12
+    mode:
+      output: true
+      input: false
+      open_drain: false
+      pullup: false
+      pulldown: false
+      analog: false
+    inverted: false
+  baud_rate: 9600
+  id: pms5003_uart
+  rx_buffer_size: 256
+  stop_bits: 1
+  data_bits: 8
+  parity: NONE
+i2c:
+- sda: 4
+  scl: 5
+  frequency: 400000.0
+  scan: true
+button:
+- platform: factory_reset
+  disabled_by_default: true
+  name: Factory Reset ESP
+  id: factory_reset_all
+  icon: mdi:restart-alert
+  entity_category: config
+  device_class: restart
+- platform: template
+  name: SenseAir S8 Calibration
+  id: senseair_s8_calibrate_button
+  on_press:
+  - then:
+    - senseair.background_calibration:
+        id: senseair_s8
+    - delay: 70s
+    - senseair.background_calibration_result:
+        id: senseair_s8
+  disabled_by_default: false
+- platform: template
+  name: SenseAir S8 Show Calibration Interval
+  id: senseair_s8_show_calibrate_interval
+  on_press:
+  - then:
+    - senseair.abc_get_period:
+        id: senseair_s8
+  disabled_by_default: false
+sensor:
+- platform: pmsx003
+  type: PMSX003
+  uart_id: pms5003_uart
+  pm_2_5:
+    name: PM 2.5
+    id: pm_2_5
+    device_class: pm25
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_1_0:
+    name: PM 1.0
+    id: pm_1_0
+    device_class: pm1
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_10_0:
+    name: PM 10.0
+    id: pm_10_0
+    device_class: pm10
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: µg/m³
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+    state_class: measurement
+  pm_0_3um:
+    name: PM 0.3
+    id: pm_0_3um
+    filters:
+    - sliding_window_moving_average:
+        window_size: 30
+        send_every: 30
+        send_first_at: 1
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: /dL
+    icon: mdi:chemical-weapon
+    accuracy_decimals: 0
+  update_interval: 0s
+- platform: template
+  name: PM 2.5 AQI
+  id: pm_2_5_aqi
+  update_interval: 5min
+  unit_of_measurement: AQI
+  icon: mdi:air-filter
+  accuracy_decimals: 0
+  filters:
+  - skip_initial: 10
+  lambda: !lambda |-
+    // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
+    // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
+    if (id(pm_2_5).state <= 12.0) {
+    // good
+    return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
+    } else if (id(pm_2_5).state <= 35.4) {
+    // moderate
+    return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
+    } else if (id(pm_2_5).state <= 55.4) {
+    // usg
+    return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
+    } else if (id(pm_2_5).state <= 150.4) {
+    // unhealthy
+    return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
+    } else if (id(pm_2_5).state <= 250.4) {
+    // very unhealthy
+    return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
+    } else if (id(pm_2_5).state <= 350.4) {
+    // hazardous
+    return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
+    } else if (id(pm_2_5).state <= 500.4) {
+    // hazardous 2
+    return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
+    } else {
+    return(500);
+    }
+  disabled_by_default: false
+  force_update: false
+- platform: senseair
+  co2:
+    name: CO2
+    id: co2
+    filters:
+    - skip_initial: 2
+    - clamp:
+        min_value: 400.0
+        max_value: .nan
+        ignore_out_of_range: false
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: ppm
+    icon: mdi:molecule-co2
+    accuracy_decimals: 0
+    device_class: carbon_dioxide
+    state_class: measurement
+  id: senseair_s8
+  uart_id: senseair_s8_uart
+  update_interval: 60s
+- platform: sht4x
+  temperature:
+    name: Temperature
+    id: temp
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: °C
+    icon: mdi:thermometer
+    accuracy_decimals: 2
+    device_class: temperature
+    state_class: measurement
+  humidity:
+    name: Humidity
+    id: humidity
+    disabled_by_default: false
+    force_update: false
+    unit_of_measurement: '%'
+    icon: mdi:water-percent
+    accuracy_decimals: 2
+    device_class: humidity
+    state_class: measurement
+  address: 0x44
+  precision: High
+  heater_power: High
+  heater_time: Long
+  heater_max_duty: 0.0
+  update_interval: 60s
+- platform: sgp4x
+  voc:
+    name: VOC Index
+    id: voc
+    disabled_by_default: false
+    force_update: false
+    icon: mdi:radiator
+    accuracy_decimals: 0
+    device_class: aqi
+    state_class: measurement
+  nox:
+    name: NOx Index
+    id: nox
+    disabled_by_default: false
+    force_update: false
+    icon: mdi:radiator
+    accuracy_decimals: 0
+    device_class: aqi
+    state_class: measurement
+  compensation:
+    temperature_source: temp
+    humidity_source: humidity
+  store_baseline: true
+  update_interval: 60s
+  address: 0x59
+- platform: wifi_signal
+  name: WiFi Signal
+  id: wifi_dbm
+  update_interval: 60s
+  disabled_by_default: false
+  force_update: false
+  unit_of_measurement: dBm
+  accuracy_decimals: 0
+  device_class: signal_strength
+  state_class: measurement
+  entity_category: diagnostic
+- platform: uptime
+  name: Uptime
+  id: device_uptime
+  disabled_by_default: false
+  force_update: false
+  unit_of_measurement: s
+  icon: mdi:timer-outline
+  accuracy_decimals: 0
+  device_class: duration
+  state_class: total_increasing
+  entity_category: diagnostic
+  update_interval: 60s
+switch:
+- platform: template
+  name: SenseAir S8 Automatic Background Calibration
+  id: senseair_s8_abc_switch
+  restore_mode: RESTORE_DEFAULT_ON
+  optimistic: true
+  entity_category: config
+  turn_on_action:
+    then:
+    - senseair.abc_enable:
+        id: senseair_s8
+  turn_off_action:
+    then:
+    - senseair.abc_disable:
+        id: senseair_s8
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Display Temperature in °F
+  icon: mdi:thermometer
+  id: display_in_f
+  restore_mode: RESTORE_DEFAULT_ON
+  optimistic: true
+  disabled_by_default: false
+  assumed_state: false
+- platform: template
+  name: Upload to AirGradient Dashboard
+  id: upload_airgradient
+  restore_mode: RESTORE_DEFAULT_OFF
+  optimistic: true
+  disabled_by_default: false
+  assumed_state: false
+- platform: safe_mode
+  name: Flash Mode (Safe Mode)
+  icon: mdi:cellphone-arrow-down
+  disabled_by_default: false
+  restore_mode: ALWAYS_OFF
+  entity_category: config
+font:
+- file:
+    family: Open Sans
+    weight: 400
+    italic: false
+    type: gfonts
+  id: open_sans_14
+  size: 14
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+- file:
+    family: Open Sans
+    weight: 400
+    italic: false
+    type: gfonts
+  id: open_sans_9
+  size: 9
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+- file:
+    family: Open Sans
+    weight: 400
+    italic: false
+    type: gfonts
+  id: open_sans_20
+  size: 20
+  glyphs:
+  - ' '
+  - '!'
+  - '"'
+  - '%'
+  - (
+  - )
+  - +
+  - ','
+  - '-'
+  - .
+  - /
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '4'
+  - '5'
+  - '6'
+  - '7'
+  - '8'
+  - '9'
+  - ':'
+  - '='
+  - A
+  - B
+  - C
+  - D
+  - E
+  - F
+  - G
+  - H
+  - I
+  - J
+  - K
+  - L
+  - M
+  - N
+  - O
+  - P
+  - Q
+  - R
+  - S
+  - T
+  - U
+  - V
+  - W
+  - X
+  - Y
+  - Z
+  - _
+  - a
+  - b
+  - c
+  - d
+  - e
+  - f
+  - g
+  - h
+  - i
+  - j
+  - k
+  - l
+  - m
+  - n
+  - o
+  - p
+  - q
+  - r
+  - s
+  - t
+  - u
+  - v
+  - w
+  - x
+  - y
+  - z
+  - °
+  - ³
+  - µ
+display:
+- platform: ssd1306_i2c
+  model: SH1106_128X64
+  id: oled_display
+  address: 0x3C
+  pages:
+  - id: summary
+    lambda: !lambda |-
+      if (id(display_in_f).state) {
+        it.printf(0, 0, id(open_sans_14), "%.1f°F", id(temp).state*9/5+32);
+      } else {
+        it.printf(0, 0, id(open_sans_14), "%.1f°C", id(temp).state);
+      }
+      it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
+      it.line(0,17,128,17);
+      it.printf(0,19, id(open_sans_9), "CO2");
+      it.printf(0,27, id(open_sans_20), "%.0f", id(co2).state);
+      it.printf(0,52, id(open_sans_9), "ppm");
+      it.line(50,19,50,64);
+      it.printf(54, 19, id(open_sans_9), "PM2.5");
+      it.printf(54, 27, id(open_sans_20), "%.0f", id(pm_2_5).state);
+      it.printf(54, 52, id(open_sans_9), "µg/m³");
+      it.line(100,19,100,64);
+      it.printf(104,18, id(open_sans_9), "TVOC");
+      it.printf(104,29, id(open_sans_9), "%.0f", id(voc).state);
+      it.printf(104,41, id(open_sans_9), "NOx");
+      it.printf(104,52, id(open_sans_9), "%.0f", id(nox).state);
+  - id: boot
+    lambda: !lambda |-
+      it.printf(0, 0, id(open_sans_14), "ID:");
+      it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%s", get_mac_address().c_str());
+      it.printf(0, 21, id(open_sans_14), "Config Ver: 2.0.0");
+      it.printf(0, 42, id(open_sans_14), "AG Pro");
+  on_page_change:
+  - to: boot
+    then:
+    - if:
+        condition:
+          lambda: !lambda |-
+            return id(device_uptime).state > 30;
+        then:
+        - display.page.show_next:
+            id: oled_display
+        - component.update:
+            id: oled_display
+  auto_clear_enabled: true
+  brightness: 1.0
+  contrast: 1.0
+  flip_x: true
+  flip_y: true
+  offset_x: 0
+  offset_y: 0
+  invert: false
+  update_interval: 1s
+interval:
+- interval: 5s
+  then:
+  - if:
+      condition:
+        lambda: !lambda |-
+          return id(device_uptime).state < 10;
+      then:
+      - display.page.show:
+          id: boot
+      else:
+      - display.page.show_next:
+          id: oled_display
+      - component.update:
+          id: oled_display
+  startup_delay: 0s
+- interval: 150s
+  then:
+  - if:
+      condition:
+        switch.is_on:
+          id: upload_airgradient
+      then:
+      - http_request.post:
+          url: !lambda |-
+            return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
+          headers:
+            Content-Type: application/json
+          json:
+            wifi: !lambda |-
+              return to_string(id(wifi_dbm).state);
+            pm01: !lambda |-
+              return to_string(id(pm_1_0).state);
+            pm02: !lambda |-
+              return to_string(id(pm_2_5).state);
+            pm10: !lambda |-
+              return to_string(id(pm_10_0).state);
+            pm003_count: !lambda |-
+              return to_string(id(pm_0_3um).state);
+            rco2: !lambda |-
+              return to_string(id(co2).state);
+            atmp: !lambda |-
+              return to_string(id(temp).state);
+            rhum: !lambda |-
+              return to_string(id(humidity).state);
+            tvoc_index: !lambda |-
+              return to_string(id(voc).state);
+            nox_index: !lambda |-
+              return to_string(id(nox).state);
+          verify_ssl: true
+          method: POST
+  startup_delay: 0s
+number:
+- platform: template
+  name: Display Contrast %
+  icon: mdi:lightbulb
+  id: display_contrast
+  min_value: 0.0
+  max_value: 100.0
+  step: 1.0
+  initial_value: 100.0
+  optimistic: true
+  restore_value: true
+  mode: SLIDER
+  on_value:
+  - then:
+    - lambda: !lambda |-
+        id(oled_display).set_contrast(id(display_contrast).state / 100.0);
+  disabled_by_default: false
+  update_interval: 60s
+http_request:
+  useragent: ESPHome
+  follow_redirects: true
+  redirect_limit: 3
+  timeout: 5s
+  esp8266_disable_ssl_support: false
+binary_sensor:
+- platform: gpio
+  pin:
+    number: 13
+    mode:
+      input: true
+      pullup: true
+      output: false
+      open_drain: false
+      pulldown: false
+      analog: false
+    inverted: true
+  internal: true
+  name: Configuration Button
+  id: config_button
+  on_multi_click:
+  - timing:
+    - state: true
+      min_length: 0ms
+      max_length: 1s
+    - state: false
+      min_length: 500ms
+    then:
+    - logger.log:
+        format: Toggling display betwen C and F
+        tag: main
+        level: DEBUG
+        args: []
+    - switch.toggle:
+        id: display_in_f
+    invalid_cooldown: 1s
+  - timing:
+    - state: true
+      min_length: 1s
+      max_length: 5s
+    - state: false
+      min_length: 500ms
+    then:
+    - logger.log:
+        format: Starting manual CO2 calibration
+        tag: main
+        level: DEBUG
+        args: []
+    - senseair.background_calibration:
+        id: senseair_s8
+    - delay: 70s
+    - senseair.background_calibration_result:
+        id: senseair_s8
+    invalid_cooldown: 1s
+  disabled_by_default: false
+api:
+  port: 6053
+  password: \033[5m''\033[6m
+  reboot_timeout: 15min
+ota:
+  safe_mode: true
+  port: 8266
+  reboot_timeout: 5min
+  num_attempts: 10
+wifi:
+  ap:
+    ap_timeout: 1min
+  domain: .local
+  reboot_timeout: 15min
+  power_save_mode: NONE
+  fast_connect: false
+  output_power: 20.0
+  passive_scan: false
+  enable_on_boot: true
+  use_address: ag-pro.local
+dashboard_import:
+  package_import_url: github://MallocArray/airgradient_esphome/airgradient-pro.yaml
+  import_full_config: false
+