sfreundel 4 роки тому
батько
коміт
eb88e90cb5
87 змінених файлів з 18319 додано та 0 видалено
  1. 23 0
      MySunTextSensor.h
  2. 159 0
      livingroom.yaml
  3. 36 0
      livingroom/platformio.ini
  4. 23 0
      livingroom/src/MySunTextSensor.h
  5. 44 0
      livingroom/src/esphome.h
  6. 9 0
      livingroom/src/esphome/README.txt
  7. 701 0
      livingroom/src/esphome/components/api/api_connection.cpp
  8. 172 0
      livingroom/src/esphome/components/api/api_connection.h
  9. 2973 0
      livingroom/src/esphome/components/api/api_pb2.cpp
  10. 745 0
      livingroom/src/esphome/components/api/api_pb2.h
  11. 552 0
      livingroom/src/esphome/components/api/api_pb2_service.cpp
  12. 185 0
      livingroom/src/esphome/components/api/api_pb2_service.h
  13. 239 0
      livingroom/src/esphome/components/api/api_server.cpp
  14. 100 0
      livingroom/src/esphome/components/api/api_server.h
  15. 214 0
      livingroom/src/esphome/components/api/custom_api_device.h
  16. 67 0
      livingroom/src/esphome/components/api/homeassistant_service.h
  17. 55 0
      livingroom/src/esphome/components/api/list_entities.cpp
  18. 52 0
      livingroom/src/esphome/components/api/list_entities.h
  19. 90 0
      livingroom/src/esphome/components/api/proto.cpp
  20. 278 0
      livingroom/src/esphome/components/api/proto.h
  21. 44 0
      livingroom/src/esphome/components/api/subscribe_state.cpp
  22. 47 0
      livingroom/src/esphome/components/api/subscribe_state.h
  23. 42 0
      livingroom/src/esphome/components/api/user_services.cpp
  24. 72 0
      livingroom/src/esphome/components/api/user_services.h
  25. 193 0
      livingroom/src/esphome/components/api/util.cpp
  26. 93 0
      livingroom/src/esphome/components/api/util.h
  27. 174 0
      livingroom/src/esphome/components/captive_portal/captive_portal.cpp
  28. 82 0
      livingroom/src/esphome/components/captive_portal/captive_portal.h
  29. 22 0
      livingroom/src/esphome/components/homeassistant/time/homeassistant_time.cpp
  30. 22 0
      livingroom/src/esphome/components/homeassistant/time/homeassistant_time.h
  31. 129 0
      livingroom/src/esphome/components/json/json_util.cpp
  32. 62 0
      livingroom/src/esphome/components/json/json_util.h
  33. 198 0
      livingroom/src/esphome/components/logger/logger.cpp
  34. 137 0
      livingroom/src/esphome/components/logger/logger.h
  35. 404 0
      livingroom/src/esphome/components/ota/ota_component.cpp
  36. 88 0
      livingroom/src/esphome/components/ota/ota_component.h
  37. 176 0
      livingroom/src/esphome/components/sun/sun.cpp
  38. 137 0
      livingroom/src/esphome/components/sun/sun.h
  39. 12 0
      livingroom/src/esphome/components/sun/text_sensor/sun_text_sensor.cpp
  40. 41 0
      livingroom/src/esphome/components/sun/text_sensor/sun_text_sensor.h
  41. 23 0
      livingroom/src/esphome/components/template/text_sensor/template_text_sensor.cpp
  42. 25 0
      livingroom/src/esphome/components/template/text_sensor/template_text_sensor.h
  43. 41 0
      livingroom/src/esphome/components/text_sensor/automation.h
  44. 33 0
      livingroom/src/esphome/components/text_sensor/text_sensor.cpp
  45. 52 0
      livingroom/src/esphome/components/text_sensor/text_sensor.h
  46. 83 0
      livingroom/src/esphome/components/time/automation.cpp
  47. 48 0
      livingroom/src/esphome/components/time/automation.h
  48. 170 0
      livingroom/src/esphome/components/time/real_time_clock.cpp
  49. 153 0
      livingroom/src/esphome/components/time/real_time_clock.h
  50. 24 0
      livingroom/src/esphome/components/version/version_text_sensor.cpp
  51. 22 0
      livingroom/src/esphome/components/version/version_text_sensor.h
  52. 704 0
      livingroom/src/esphome/components/web_server/web_server.cpp
  53. 176 0
      livingroom/src/esphome/components/web_server/web_server.h
  54. 96 0
      livingroom/src/esphome/components/web_server_base/web_server_base.cpp
  55. 76 0
      livingroom/src/esphome/components/web_server_base/web_server_base.h
  56. 639 0
      livingroom/src/esphome/components/wifi/wifi_component.cpp
  57. 301 0
      livingroom/src/esphome/components/wifi/wifi_component.h
  58. 612 0
      livingroom/src/esphome/components/wifi/wifi_component_esp32.cpp
  59. 731 0
      livingroom/src/esphome/components/wifi/wifi_component_esp8266.cpp
  60. 163 0
      livingroom/src/esphome/core/application.cpp
  61. 253 0
      livingroom/src/esphome/core/application.h
  62. 212 0
      livingroom/src/esphome/core/automation.h
  63. 275 0
      livingroom/src/esphome/core/base_automation.h
  64. 251 0
      livingroom/src/esphome/core/color.h
  65. 186 0
      livingroom/src/esphome/core/component.cpp
  66. 268 0
      livingroom/src/esphome/core/component.h
  67. 58 0
      livingroom/src/esphome/core/controller.cpp
  68. 60 0
      livingroom/src/esphome/core/controller.h
  69. 10 0
      livingroom/src/esphome/core/defines.h
  70. 317 0
      livingroom/src/esphome/core/esphal.cpp
  71. 127 0
      livingroom/src/esphome/core/esphal.h
  72. 333 0
      livingroom/src/esphome/core/helpers.cpp
  73. 320 0
      livingroom/src/esphome/core/helpers.h
  74. 62 0
      livingroom/src/esphome/core/log.cpp
  75. 164 0
      livingroom/src/esphome/core/log.h
  76. 214 0
      livingroom/src/esphome/core/optional.h
  77. 308 0
      livingroom/src/esphome/core/preferences.cpp
  78. 109 0
      livingroom/src/esphome/core/preferences.h
  79. 271 0
      livingroom/src/esphome/core/scheduler.cpp
  80. 67 0
      livingroom/src/esphome/core/scheduler.h
  81. 102 0
      livingroom/src/esphome/core/util.cpp
  82. 22 0
      livingroom/src/esphome/core/util.h
  83. 2 0
      livingroom/src/esphome/core/version.h
  84. 419 0
      livingroom/src/main.cpp
  85. 587 0
      livingroom/src/sunset.cpp
  86. 129 0
      livingroom/src/sunset.h
  87. 129 0
      sunset.h

+ 23 - 0
MySunTextSensor.h

@@ -0,0 +1,23 @@
+#include "esphome.h"
+#include "sunset.h"
+
+
+class MySunTextSensor : public PollingComponent, public TextSensor {
+public:
+  class SunSet *sun;
+
+  // constructor
+  MySunTextSensor() : PollingComponent(15000) {
+    sun = new SunSet();  }
+
+  // float get_setup_priority() const override { return esphome::setup_priority::XXXX; }
+
+  void setup() override {
+    // This will be called by App.setup()
+  }
+  void update() override {
+    // This will be called every "update_interval" milliseconds.
+  }
+
+
+};

+ 159 - 0
livingroom.yaml

@@ -0,0 +1,159 @@
+esphome:
+  name: livingroom
+  platform: ESP8266
+  board: esp01_1m
+  #  board: esp12e
+  includes: 
+    - MySunTextSensor.h
+
+# Enable logging
+logger:
+
+# Enable Home Assistant API
+api:
+
+ota:
+
+substitutions:
+  #Shift sunset and sunrisen on minute
+  shift_sunset: '+17' 
+  shift_sunrise: '+6' 
+
+# At least one time source is required
+time:
+  - platform: homeassistant     # sntp
+    timezone: "Europe/Berlin"
+    id: esptime                 # sntp_time
+    # - minutes: !lambda 'return id(heating_on_time_1_minutes).state;'
+    #   hours: !lambda 'return id(heating_on_time_1_hours).state;'
+
+# At WiFi setup
+wifi:
+  ssid: "WLAN-LX"
+  password: "SP-RAXX749x7"
+
+  # Enable fallback hotspot (captive portal) in case wifi connection fails
+  ap:
+    ssid: "Livingroom Fallback Hotspot"
+    password: "R8ZKoXbtquSv"
+
+# Example configuration entry
+web_server:
+  port: 80
+
+# determine Sunrise and Sunset based on geographical location
+sun:
+  latitude: 48.77034 
+  longitude: 8.34306
+
+  on_sunrise:
+    - then:
+      - logger.log: Good morning!
+    # Custom elevation, will be called shortly after the trigger above.
+    #- elevation: 5°
+    #  then:
+    #  - logger.log: Good morning 2!
+
+  on_sunset:
+    - then:
+      - logger.log: Good evening!
+
+captive_portal:
+
+# Example configuration entry
+text_sensor:
+  - platform: sun
+    name: Sun Next Sunrise
+    type: sunrise
+    
+  - platform: sun
+    name: Sun Next Sunset
+    type: sunset
+
+  - platform: version
+    name: Platform
+    on_value:
+      then:
+        - lambda: |-
+            ESP_LOGD("main", "The current version is %s", x.c_str()); 
+
+  - platform: template
+    name: "Template Text Sensor"
+    lambda: |-
+      char str[17];
+      time_t currTime = id(esptime).now().timestamp ;
+      strftime(str, sizeof(str), "%Y-%m-%d %H:%M", localtime(&currTime));
+      return {str};
+    update_interval: 60s      
+
+  - platform: template
+    name: "Sunrise"
+    id: sunrise_shift
+    lambda: |-
+      auto sunrise = id(sun_sun).sunrise();
+      auto time = id(esptime).now();
+      sun_sun->set_latitude(48.77034);
+      sun_sun->set_longitude(8.34306);
+      if (!sunrise.has_value())
+        return {"NaN"};
+      int sunrise_min = sunrise.value().hour * 60 + sunrise.value().minute + ${shift_sunrise} ;
+      int sunrise_hour = int(sunrise_min / 60);
+      int sunrise_minute = sunrise_min % 60;
+      ESP_LOGD("sunrise_shift", "sunrise.value.()hour is %i , sunrise.value().minute is %i ", sunrise.value().hour , sunrise.value().minute);
+      ESP_LOGD("sunrise_shift", "sunrise_min is %i , sunrise_hour is %i ,sunrise_minute is %i ", sunrise_min , sunrise_hour , sunrise_minute);
+      char buffer[6];
+      sprintf(buffer,"%02i:%02i", sunrise_hour , sunrise_minute);
+      return {buffer};
+
+#      ESP_LOGD("sunset_shift", "state relay is %i  ", id(relay).state );
+#      if((sunrise_hour == time.hour) and (sunrise_minute == time.minute))
+#        if(id(relay).state)
+#         id(relay).turn_off();
+#         ESP_LOGD("sunset_shift", "change relay is OFF" );
+
+
+  - platform: template
+    name: "Sunset"
+    id: sunset_shift
+    lambda: |-
+      auto sunset = id(sun_sun).sunset();
+      auto time = id(esptime).now();
+      if (!sunset.has_value())
+        return {"NaN"};
+      int sunset_min = sunset.value().hour * 60 + sunset.value().minute + ${shift_sunset};
+      int sunset_hour = int(sunset_min / 60);
+      int sunset_minute = sunset_min % 60;
+      ESP_LOGD("sunset_shift", "sunset.value.()hour is %i , sunset.value().minute is %i ", sunset.value().hour , sunset.value().minute);
+      ESP_LOGD("sunset_shift", "sunset_min is %i , sunset_hour is %i ,sunset_minute is %i ", sunset_min , sunset_hour , sunset_minute);
+      char buffer[6];
+      sprintf(buffer,"%02i:%02i", sunset_hour , sunset_minute);
+      return {buffer};
+ 
+#      ESP_LOGD("sunset_shift", "state relay is %i  ", id(relay).state );
+#      if((sunset_hour == time.hour) and (sunset_minute == time.minute))
+#        if(!id(relay).state)
+#         id(relay).turn_on();
+#         ESP_LOGD("sunset_shift", "change relay is ON" );      
+
+  - platform: template
+    name: "Sunset Algo"
+    lambda: |-
+      int sunrise;
+      int sunset;
+      auto sunSensor = new MySunTextSensor();
+      App.register_component(sunSensor);
+      auto sun_sensor = sunSensor->sun;
+      auto time = id(esptime).now();
+      sun_sensor->setPosition(48.77034, 8.34306, 1);
+      sun_sensor->setCurrentDate(2021, 03, 05);
+      sunrise = static_cast<int>(sun_sensor->calcSunrise());
+      sunset = static_cast<int>(sun_sensor->calcSunset());
+      char buffer[100];
+      
+      sprintf(buffer, "Sunrise at %.2d:%.2dam, Sunset at %.2d:%.2dpm", (sunrise/60), (sunrise%60), (sunset/60), (sunset%60));
+      return {buffer};
+
+# sun_sensor->setCurrentDate(time.year(), time.month(), time.day());
+
+#text_sensors:
+#    name: "My Custom Text Sensor"

+ 36 - 0
livingroom/platformio.ini

@@ -0,0 +1,36 @@
+; Auto generated code by esphome
+
+[common]
+lib_deps =
+build_flags =
+upload_flags =
+
+; ===== DO NOT EDIT ANYTHING BELOW THIS LINE =====
+; ========== AUTO GENERATED CODE BEGIN ===========
+[env:livingroom]
+board = esp01_1m
+board_build.flash_mode = dout
+board_build.ldscript = eagle.flash.1m.ld
+build_flags =
+    -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_DEBUG
+    -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH
+    -DUSE_STORE_LOG_STR_IN_FLASH
+    -Wno-sign-compare
+    -Wno-unused-but-set-variable
+    -Wno-unused-variable
+    -fno-exceptions
+    ${common.build_flags}
+framework = arduino
+lib_deps =
+    ESPAsyncTCP-esphome@1.2.3
+    ESP8266WiFi
+    ESP8266mDNS
+    ESPAsyncWebServer-esphome@1.2.7
+    Update
+    ArduinoJson-esphomelib@5.13.3
+    ${common.lib_deps}
+platform = espressif8266@2.6.2
+upload_speed = 115200
+; =========== AUTO GENERATED CODE END ============
+; ========= YOU CAN EDIT AFTER THIS LINE =========
+

+ 23 - 0
livingroom/src/MySunTextSensor.h

@@ -0,0 +1,23 @@
+#include "esphome.h"
+#include "sunset.h"
+
+
+class MySunTextSensor : public PollingComponent, public TextSensor {
+public:
+  class SunSet *sun;
+
+  // constructor
+  MySunTextSensor() : PollingComponent(15000) {
+    sun = new SunSet();  }
+
+  // float get_setup_priority() const override { return esphome::setup_priority::XXXX; }
+
+  void setup() override {
+    // This will be called by App.setup()
+  }
+  void update() override {
+    // This will be called every "update_interval" milliseconds.
+  }
+
+
+};

+ 44 - 0
livingroom/src/esphome.h

@@ -0,0 +1,44 @@
+#pragma once
+#include "esphome/components/api/api_connection.h"
+#include "esphome/components/api/api_pb2.h"
+#include "esphome/components/api/api_pb2_service.h"
+#include "esphome/components/api/api_server.h"
+#include "esphome/components/api/custom_api_device.h"
+#include "esphome/components/api/homeassistant_service.h"
+#include "esphome/components/api/list_entities.h"
+#include "esphome/components/api/proto.h"
+#include "esphome/components/api/subscribe_state.h"
+#include "esphome/components/api/user_services.h"
+#include "esphome/components/api/util.h"
+#include "esphome/components/captive_portal/captive_portal.h"
+#include "esphome/components/homeassistant/time/homeassistant_time.h"
+#include "esphome/components/json/json_util.h"
+#include "esphome/components/logger/logger.h"
+#include "esphome/components/ota/ota_component.h"
+#include "esphome/components/sun/sun.h"
+#include "esphome/components/sun/text_sensor/sun_text_sensor.h"
+#include "esphome/components/template/text_sensor/template_text_sensor.h"
+#include "esphome/components/text_sensor/automation.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+#include "esphome/components/time/automation.h"
+#include "esphome/components/time/real_time_clock.h"
+#include "esphome/components/version/version_text_sensor.h"
+#include "esphome/components/web_server/web_server.h"
+#include "esphome/components/web_server_base/web_server_base.h"
+#include "esphome/components/wifi/wifi_component.h"
+#include "esphome/core/application.h"
+#include "esphome/core/automation.h"
+#include "esphome/core/base_automation.h"
+#include "esphome/core/color.h"
+#include "esphome/core/component.h"
+#include "esphome/core/controller.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/esphal.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+#include "esphome/core/optional.h"
+#include "esphome/core/preferences.h"
+#include "esphome/core/scheduler.h"
+#include "esphome/core/util.h"
+#include "esphome/core/version.h"
+

+ 9 - 0
livingroom/src/esphome/README.txt

@@ -0,0 +1,9 @@
+
+THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
+
+ESPHome automatically populates the esphome/ directory, and any
+changes to this directory will be removed the next time esphome is
+run.
+
+For modifying esphome's core files, please use a development esphome install
+or use the custom_components folder.

+ 701 - 0
livingroom/src/esphome/components/api/api_connection.cpp

@@ -0,0 +1,701 @@
+#include "api_connection.h"
+#include "esphome/core/log.h"
+#include "esphome/core/util.h"
+#include "esphome/core/version.h"
+
+#ifdef USE_DEEP_SLEEP
+#include "esphome/components/deep_sleep/deep_sleep_component.h"
+#endif
+#ifdef USE_HOMEASSISTANT_TIME
+#include "esphome/components/homeassistant/time/homeassistant_time.h"
+#endif
+
+namespace esphome {
+namespace api {
+
+static const char *TAG = "api.connection";
+
+APIConnection::APIConnection(AsyncClient *client, APIServer *parent)
+    : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
+  this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this);
+  this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this);
+  this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); },
+                           this);
+  this->client_->onData([](void *s, AsyncClient *c, void *buf,
+                           size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); },
+                        this);
+
+  this->send_buffer_.reserve(64);
+  this->recv_buffer_.reserve(32);
+  this->client_info_ = this->client_->remoteIP().toString().c_str();
+  this->last_traffic_ = millis();
+}
+APIConnection::~APIConnection() { delete this->client_; }
+void APIConnection::on_error_(int8_t error) { this->remove_ = true; }
+void APIConnection::on_disconnect_() { this->remove_ = true; }
+void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); }
+void APIConnection::on_data_(uint8_t *buf, size_t len) {
+  if (len == 0 || buf == nullptr)
+    return;
+  this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len);
+}
+void APIConnection::parse_recv_buffer_() {
+  if (this->recv_buffer_.empty() || this->remove_)
+    return;
+
+  while (!this->recv_buffer_.empty()) {
+    if (this->recv_buffer_[0] != 0x00) {
+      ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str());
+      this->on_fatal_error();
+      return;
+    }
+    uint32_t i = 1;
+    const uint32_t size = this->recv_buffer_.size();
+    uint32_t consumed;
+    auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
+    if (!msg_size_varint.has_value())
+      // not enough data there yet
+      return;
+    i += consumed;
+    uint32_t msg_size = msg_size_varint->as_uint32();
+
+    auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
+    if (!msg_type_varint.has_value())
+      // not enough data there yet
+      return;
+    i += consumed;
+    uint32_t msg_type = msg_type_varint->as_uint32();
+
+    if (size - i < msg_size)
+      // message body not fully received
+      return;
+
+    uint8_t *msg = &this->recv_buffer_[i];
+    this->read_message(msg_size, msg_type, msg);
+    if (this->remove_)
+      return;
+    // pop front
+    uint32_t total = i + msg_size;
+    this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total);
+    this->last_traffic_ = millis();
+  }
+}
+
+void APIConnection::disconnect_client() {
+  this->client_->close();
+  this->remove_ = true;
+}
+
+void APIConnection::loop() {
+  if (this->remove_)
+    return;
+
+  if (this->next_close_) {
+    this->disconnect_client();
+    return;
+  }
+
+  if (!network_is_connected()) {
+    // when network is disconnected force disconnect immediately
+    // don't wait for timeout
+    this->on_fatal_error();
+    return;
+  }
+  if (this->client_->disconnected()) {
+    // failsafe for disconnect logic
+    this->on_disconnect_();
+    return;
+  }
+  this->parse_recv_buffer_();
+
+  this->list_entities_iterator_.advance();
+  this->initial_state_iterator_.advance();
+
+  const uint32_t keepalive = 60000;
+  if (this->sent_ping_) {
+    // Disconnect if not responded within 2.5*keepalive
+    if (millis() - this->last_traffic_ > (keepalive * 5) / 2) {
+      ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
+      this->disconnect_client();
+    }
+  } else if (millis() - this->last_traffic_ > keepalive) {
+    this->sent_ping_ = true;
+    this->send_ping_request(PingRequest());
+  }
+
+#ifdef USE_ESP32_CAMERA
+  if (this->image_reader_.available()) {
+    uint32_t space = this->client_->space();
+    // reserve 15 bytes for metadata, and at least 64 bytes of data
+    if (space >= 15 + 64) {
+      uint32_t to_send = std::min(space - 15, this->image_reader_.available());
+      auto buffer = this->create_buffer();
+      // fixed32 key = 1;
+      buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
+      // bytes data = 2;
+      buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
+      // bool done = 3;
+      bool done = this->image_reader_.available() == to_send;
+      buffer.encode_bool(3, done);
+      bool success = this->send_buffer(buffer, 44);
+
+      if (success) {
+        this->image_reader_.consume_data(to_send);
+      }
+      if (success && done) {
+        this->image_reader_.return_image();
+      }
+    }
+  }
+#endif
+}
+
+std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
+  return App.get_name() + component_type + nameable->get_object_id();
+}
+
+#ifdef USE_BINARY_SENSOR
+bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) {
+  if (!this->state_subscription_)
+    return false;
+
+  BinarySensorStateResponse resp;
+  resp.key = binary_sensor->get_object_id_hash();
+  resp.state = state;
+  resp.missing_state = !binary_sensor->has_state();
+  return this->send_binary_sensor_state_response(resp);
+}
+bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) {
+  ListEntitiesBinarySensorResponse msg;
+  msg.object_id = binary_sensor->get_object_id();
+  msg.key = binary_sensor->get_object_id_hash();
+  msg.name = binary_sensor->get_name();
+  msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor);
+  msg.device_class = binary_sensor->get_device_class();
+  msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
+  return this->send_list_entities_binary_sensor_response(msg);
+}
+#endif
+
+#ifdef USE_COVER
+bool APIConnection::send_cover_state(cover::Cover *cover) {
+  if (!this->state_subscription_)
+    return false;
+
+  auto traits = cover->get_traits();
+  CoverStateResponse resp{};
+  resp.key = cover->get_object_id_hash();
+  resp.legacy_state =
+      (cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED;
+  resp.position = cover->position;
+  if (traits.get_supports_tilt())
+    resp.tilt = cover->tilt;
+  resp.current_operation = static_cast<enums::CoverOperation>(cover->current_operation);
+  return this->send_cover_state_response(resp);
+}
+bool APIConnection::send_cover_info(cover::Cover *cover) {
+  auto traits = cover->get_traits();
+  ListEntitiesCoverResponse msg;
+  msg.key = cover->get_object_id_hash();
+  msg.object_id = cover->get_object_id();
+  msg.name = cover->get_name();
+  msg.unique_id = get_default_unique_id("cover", cover);
+  msg.assumed_state = traits.get_is_assumed_state();
+  msg.supports_position = traits.get_supports_position();
+  msg.supports_tilt = traits.get_supports_tilt();
+  msg.device_class = cover->get_device_class();
+  return this->send_list_entities_cover_response(msg);
+}
+void APIConnection::cover_command(const CoverCommandRequest &msg) {
+  cover::Cover *cover = App.get_cover_by_key(msg.key);
+  if (cover == nullptr)
+    return;
+
+  auto call = cover->make_call();
+  if (msg.has_legacy_command) {
+    switch (msg.legacy_command) {
+      case enums::LEGACY_COVER_COMMAND_OPEN:
+        call.set_command_open();
+        break;
+      case enums::LEGACY_COVER_COMMAND_CLOSE:
+        call.set_command_close();
+        break;
+      case enums::LEGACY_COVER_COMMAND_STOP:
+        call.set_command_stop();
+        break;
+    }
+  }
+  if (msg.has_position)
+    call.set_position(msg.position);
+  if (msg.has_tilt)
+    call.set_tilt(msg.tilt);
+  if (msg.stop)
+    call.set_command_stop();
+  call.perform();
+}
+#endif
+
+#ifdef USE_FAN
+bool APIConnection::send_fan_state(fan::FanState *fan) {
+  if (!this->state_subscription_)
+    return false;
+
+  auto traits = fan->get_traits();
+  FanStateResponse resp{};
+  resp.key = fan->get_object_id_hash();
+  resp.state = fan->state;
+  if (traits.supports_oscillation())
+    resp.oscillating = fan->oscillating;
+  if (traits.supports_speed())
+    resp.speed = static_cast<enums::FanSpeed>(fan->speed);
+  if (traits.supports_direction())
+    resp.direction = static_cast<enums::FanDirection>(fan->direction);
+  return this->send_fan_state_response(resp);
+}
+bool APIConnection::send_fan_info(fan::FanState *fan) {
+  auto traits = fan->get_traits();
+  ListEntitiesFanResponse msg;
+  msg.key = fan->get_object_id_hash();
+  msg.object_id = fan->get_object_id();
+  msg.name = fan->get_name();
+  msg.unique_id = get_default_unique_id("fan", fan);
+  msg.supports_oscillation = traits.supports_oscillation();
+  msg.supports_speed = traits.supports_speed();
+  msg.supports_direction = traits.supports_direction();
+  return this->send_list_entities_fan_response(msg);
+}
+void APIConnection::fan_command(const FanCommandRequest &msg) {
+  fan::FanState *fan = App.get_fan_by_key(msg.key);
+  if (fan == nullptr)
+    return;
+
+  auto call = fan->make_call();
+  if (msg.has_state)
+    call.set_state(msg.state);
+  if (msg.has_oscillating)
+    call.set_oscillating(msg.oscillating);
+  if (msg.has_speed)
+    call.set_speed(static_cast<fan::FanSpeed>(msg.speed));
+  if (msg.has_direction)
+    call.set_direction(static_cast<fan::FanDirection>(msg.direction));
+  call.perform();
+}
+#endif
+
+#ifdef USE_LIGHT
+bool APIConnection::send_light_state(light::LightState *light) {
+  if (!this->state_subscription_)
+    return false;
+
+  auto traits = light->get_traits();
+  auto values = light->remote_values;
+  LightStateResponse resp{};
+
+  resp.key = light->get_object_id_hash();
+  resp.state = values.is_on();
+  if (traits.get_supports_brightness())
+    resp.brightness = values.get_brightness();
+  if (traits.get_supports_rgb()) {
+    resp.red = values.get_red();
+    resp.green = values.get_green();
+    resp.blue = values.get_blue();
+  }
+  if (traits.get_supports_rgb_white_value())
+    resp.white = values.get_white();
+  if (traits.get_supports_color_temperature())
+    resp.color_temperature = values.get_color_temperature();
+  if (light->supports_effects())
+    resp.effect = light->get_effect_name();
+  return this->send_light_state_response(resp);
+}
+bool APIConnection::send_light_info(light::LightState *light) {
+  auto traits = light->get_traits();
+  ListEntitiesLightResponse msg;
+  msg.key = light->get_object_id_hash();
+  msg.object_id = light->get_object_id();
+  msg.name = light->get_name();
+  msg.unique_id = get_default_unique_id("light", light);
+  msg.supports_brightness = traits.get_supports_brightness();
+  msg.supports_rgb = traits.get_supports_rgb();
+  msg.supports_white_value = traits.get_supports_rgb_white_value();
+  msg.supports_color_temperature = traits.get_supports_color_temperature();
+  if (msg.supports_color_temperature) {
+    msg.min_mireds = traits.get_min_mireds();
+    msg.max_mireds = traits.get_max_mireds();
+  }
+  if (light->supports_effects()) {
+    msg.effects.emplace_back("None");
+    for (auto *effect : light->get_effects())
+      msg.effects.push_back(effect->get_name());
+  }
+  return this->send_list_entities_light_response(msg);
+}
+void APIConnection::light_command(const LightCommandRequest &msg) {
+  light::LightState *light = App.get_light_by_key(msg.key);
+  if (light == nullptr)
+    return;
+
+  auto call = light->make_call();
+  if (msg.has_state)
+    call.set_state(msg.state);
+  if (msg.has_brightness)
+    call.set_brightness(msg.brightness);
+  if (msg.has_rgb) {
+    call.set_red(msg.red);
+    call.set_green(msg.green);
+    call.set_blue(msg.blue);
+  }
+  if (msg.has_white)
+    call.set_white(msg.white);
+  if (msg.has_color_temperature)
+    call.set_color_temperature(msg.color_temperature);
+  if (msg.has_transition_length)
+    call.set_transition_length(msg.transition_length);
+  if (msg.has_flash_length)
+    call.set_flash_length(msg.flash_length);
+  if (msg.has_effect)
+    call.set_effect(msg.effect);
+  call.perform();
+}
+#endif
+
+#ifdef USE_SENSOR
+bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) {
+  if (!this->state_subscription_)
+    return false;
+
+  SensorStateResponse resp{};
+  resp.key = sensor->get_object_id_hash();
+  resp.state = state;
+  resp.missing_state = !sensor->has_state();
+  return this->send_sensor_state_response(resp);
+}
+bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
+  ListEntitiesSensorResponse msg;
+  msg.key = sensor->get_object_id_hash();
+  msg.object_id = sensor->get_object_id();
+  msg.name = sensor->get_name();
+  msg.unique_id = sensor->unique_id();
+  if (msg.unique_id.empty())
+    msg.unique_id = get_default_unique_id("sensor", sensor);
+  msg.icon = sensor->get_icon();
+  msg.unit_of_measurement = sensor->get_unit_of_measurement();
+  msg.accuracy_decimals = sensor->get_accuracy_decimals();
+  msg.force_update = sensor->get_force_update();
+  return this->send_list_entities_sensor_response(msg);
+}
+#endif
+
+#ifdef USE_SWITCH
+bool APIConnection::send_switch_state(switch_::Switch *a_switch, bool state) {
+  if (!this->state_subscription_)
+    return false;
+
+  SwitchStateResponse resp{};
+  resp.key = a_switch->get_object_id_hash();
+  resp.state = state;
+  return this->send_switch_state_response(resp);
+}
+bool APIConnection::send_switch_info(switch_::Switch *a_switch) {
+  ListEntitiesSwitchResponse msg;
+  msg.key = a_switch->get_object_id_hash();
+  msg.object_id = a_switch->get_object_id();
+  msg.name = a_switch->get_name();
+  msg.unique_id = get_default_unique_id("switch", a_switch);
+  msg.icon = a_switch->get_icon();
+  msg.assumed_state = a_switch->assumed_state();
+  return this->send_list_entities_switch_response(msg);
+}
+void APIConnection::switch_command(const SwitchCommandRequest &msg) {
+  switch_::Switch *a_switch = App.get_switch_by_key(msg.key);
+  if (a_switch == nullptr)
+    return;
+
+  if (msg.state)
+    a_switch->turn_on();
+  else
+    a_switch->turn_off();
+}
+#endif
+
+#ifdef USE_TEXT_SENSOR
+bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state) {
+  if (!this->state_subscription_)
+    return false;
+
+  TextSensorStateResponse resp{};
+  resp.key = text_sensor->get_object_id_hash();
+  resp.state = std::move(state);
+  resp.missing_state = !text_sensor->has_state();
+  return this->send_text_sensor_state_response(resp);
+}
+bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) {
+  ListEntitiesTextSensorResponse msg;
+  msg.key = text_sensor->get_object_id_hash();
+  msg.object_id = text_sensor->get_object_id();
+  msg.name = text_sensor->get_name();
+  msg.unique_id = text_sensor->unique_id();
+  if (msg.unique_id.empty())
+    msg.unique_id = get_default_unique_id("text_sensor", text_sensor);
+  msg.icon = text_sensor->get_icon();
+  return this->send_list_entities_text_sensor_response(msg);
+}
+#endif
+
+#ifdef USE_CLIMATE
+bool APIConnection::send_climate_state(climate::Climate *climate) {
+  if (!this->state_subscription_)
+    return false;
+
+  auto traits = climate->get_traits();
+  ClimateStateResponse resp{};
+  resp.key = climate->get_object_id_hash();
+  resp.mode = static_cast<enums::ClimateMode>(climate->mode);
+  resp.action = static_cast<enums::ClimateAction>(climate->action);
+  if (traits.get_supports_current_temperature())
+    resp.current_temperature = climate->current_temperature;
+  if (traits.get_supports_two_point_target_temperature()) {
+    resp.target_temperature_low = climate->target_temperature_low;
+    resp.target_temperature_high = climate->target_temperature_high;
+  } else {
+    resp.target_temperature = climate->target_temperature;
+  }
+  if (traits.get_supports_away())
+    resp.away = climate->away;
+  if (traits.get_supports_fan_modes())
+    resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode);
+  if (traits.get_supports_swing_modes())
+    resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
+  return this->send_climate_state_response(resp);
+}
+bool APIConnection::send_climate_info(climate::Climate *climate) {
+  auto traits = climate->get_traits();
+  ListEntitiesClimateResponse msg;
+  msg.key = climate->get_object_id_hash();
+  msg.object_id = climate->get_object_id();
+  msg.name = climate->get_name();
+  msg.unique_id = get_default_unique_id("climate", climate);
+  msg.supports_current_temperature = traits.get_supports_current_temperature();
+  msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
+  for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
+                    climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY}) {
+    if (traits.supports_mode(mode))
+      msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
+  }
+  msg.visual_min_temperature = traits.get_visual_min_temperature();
+  msg.visual_max_temperature = traits.get_visual_max_temperature();
+  msg.visual_temperature_step = traits.get_visual_temperature_step();
+  msg.supports_away = traits.get_supports_away();
+  msg.supports_action = traits.get_supports_action();
+  for (auto fan_mode : {climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, climate::CLIMATE_FAN_AUTO,
+                        climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
+                        climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE}) {
+    if (traits.supports_fan_mode(fan_mode))
+      msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
+  }
+  for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
+                          climate::CLIMATE_SWING_HORIZONTAL}) {
+    if (traits.supports_swing_mode(swing_mode))
+      msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode));
+  }
+  return this->send_list_entities_climate_response(msg);
+}
+void APIConnection::climate_command(const ClimateCommandRequest &msg) {
+  climate::Climate *climate = App.get_climate_by_key(msg.key);
+  if (climate == nullptr)
+    return;
+
+  auto call = climate->make_call();
+  if (msg.has_mode)
+    call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
+  if (msg.has_target_temperature)
+    call.set_target_temperature(msg.target_temperature);
+  if (msg.has_target_temperature_low)
+    call.set_target_temperature_low(msg.target_temperature_low);
+  if (msg.has_target_temperature_high)
+    call.set_target_temperature_high(msg.target_temperature_high);
+  if (msg.has_away)
+    call.set_away(msg.away);
+  if (msg.has_fan_mode)
+    call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
+  if (msg.has_swing_mode)
+    call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
+  call.perform();
+}
+#endif
+
+#ifdef USE_ESP32_CAMERA
+void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
+  if (!this->state_subscription_)
+    return;
+  if (this->image_reader_.available())
+    return;
+  this->image_reader_.set_image(image);
+}
+bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) {
+  ListEntitiesCameraResponse msg;
+  msg.key = camera->get_object_id_hash();
+  msg.object_id = camera->get_object_id();
+  msg.name = camera->get_name();
+  msg.unique_id = get_default_unique_id("camera", camera);
+  return this->send_list_entities_camera_response(msg);
+}
+void APIConnection::camera_image(const CameraImageRequest &msg) {
+  if (esp32_camera::global_esp32_camera == nullptr)
+    return;
+
+  if (msg.single)
+    esp32_camera::global_esp32_camera->request_image();
+  if (msg.stream)
+    esp32_camera::global_esp32_camera->request_stream();
+}
+#endif
+
+#ifdef USE_HOMEASSISTANT_TIME
+void APIConnection::on_get_time_response(const GetTimeResponse &value) {
+  if (homeassistant::global_homeassistant_time != nullptr)
+    homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
+}
+#endif
+
+bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
+  if (this->log_subscription_ < level)
+    return false;
+
+  // Send raw so that we don't copy too much
+  auto buffer = this->create_buffer();
+  // LogLevel level = 1;
+  buffer.encode_uint32(1, static_cast<uint32_t>(level));
+  // string tag = 2;
+  // buffer.encode_string(2, tag, strlen(tag));
+  // string message = 3;
+  buffer.encode_string(3, line, strlen(line));
+  // SubscribeLogsResponse - 29
+  bool success = this->send_buffer(buffer, 29);
+  if (!success) {
+    buffer = this->create_buffer();
+    // bool send_failed = 4;
+    buffer.encode_bool(4, true);
+    return this->send_buffer(buffer, 29);
+  } else {
+    return true;
+  }
+}
+
+HelloResponse APIConnection::hello(const HelloRequest &msg) {
+  this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str();
+  this->client_info_ += ")";
+  ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str());
+
+  HelloResponse resp;
+  resp.api_version_major = 1;
+  resp.api_version_minor = 3;
+  resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
+  this->connection_state_ = ConnectionState::CONNECTED;
+  return resp;
+}
+ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
+  bool correct = this->parent_->check_password(msg.password);
+
+  ConnectResponse resp;
+  // bool invalid_password = 1;
+  resp.invalid_password = !correct;
+  if (correct) {
+    ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str());
+    this->connection_state_ = ConnectionState::AUTHENTICATED;
+
+#ifdef USE_HOMEASSISTANT_TIME
+    if (homeassistant::global_homeassistant_time != nullptr) {
+      this->send_time_request();
+    }
+#endif
+  }
+  return resp;
+}
+DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
+  DeviceInfoResponse resp{};
+  resp.uses_password = this->parent_->uses_password();
+  resp.name = App.get_name();
+  resp.mac_address = get_mac_address_pretty();
+  resp.esphome_version = ESPHOME_VERSION;
+  resp.compilation_time = App.get_compilation_time();
+#ifdef ARDUINO_BOARD
+  resp.model = ARDUINO_BOARD;
+#endif
+#ifdef USE_DEEP_SLEEP
+  resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
+#endif
+  return resp;
+}
+void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
+  for (auto &it : this->parent_->get_state_subs())
+    if (it.entity_id == msg.entity_id)
+      it.callback(msg.state);
+}
+void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
+  bool found = false;
+  for (auto *service : this->parent_->get_user_services()) {
+    if (service->execute_service(msg)) {
+      found = true;
+    }
+  }
+  if (!found) {
+    ESP_LOGV(TAG, "Could not find matching service!");
+  }
+}
+void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
+  for (auto &it : this->parent_->get_state_subs()) {
+    SubscribeHomeAssistantStateResponse resp;
+    resp.entity_id = it.entity_id;
+    if (!this->send_subscribe_home_assistant_state_response(resp)) {
+      this->on_fatal_error();
+      return;
+    }
+  }
+}
+bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
+  if (this->remove_)
+    return false;
+
+  std::vector<uint8_t> header;
+  header.push_back(0x00);
+  ProtoVarInt(buffer.get_buffer()->size()).encode(header);
+  ProtoVarInt(message_type).encode(header);
+
+  size_t needed_space = buffer.get_buffer()->size() + header.size();
+
+  if (needed_space > this->client_->space()) {
+    delay(0);
+    if (needed_space > this->client_->space()) {
+      // SubscribeLogsResponse
+      if (message_type != 29) {
+        ESP_LOGV(TAG, "Cannot send message because of TCP buffer space");
+      }
+      delay(0);
+      return false;
+    }
+  }
+
+  this->client_->add(reinterpret_cast<char *>(header.data()), header.size(),
+                     ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE);
+  this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size(),
+                     ASYNC_WRITE_FLAG_COPY);
+  bool ret = this->client_->send();
+  return ret;
+}
+void APIConnection::on_unauthenticated_access() {
+  ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str());
+  this->on_fatal_error();
+}
+void APIConnection::on_no_setup_connection() {
+  ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str());
+  this->on_fatal_error();
+}
+void APIConnection::on_fatal_error() {
+  ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str());
+  this->client_->close();
+  this->remove_ = true;
+}
+
+}  // namespace api
+}  // namespace esphome

+ 172 - 0
livingroom/src/esphome/components/api/api_connection.h

@@ -0,0 +1,172 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/application.h"
+#include "api_pb2.h"
+#include "api_pb2_service.h"
+#include "api_server.h"
+
+namespace esphome {
+namespace api {
+
+class APIConnection : public APIServerConnection {
+ public:
+  APIConnection(AsyncClient *client, APIServer *parent);
+  virtual ~APIConnection();
+
+  void disconnect_client();
+  void loop();
+
+  bool send_list_info_done() {
+    ListEntitiesDoneResponse resp;
+    return this->send_list_entities_done_response(resp);
+  }
+#ifdef USE_BINARY_SENSOR
+  bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
+  bool send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
+#endif
+#ifdef USE_COVER
+  bool send_cover_state(cover::Cover *cover);
+  bool send_cover_info(cover::Cover *cover);
+  void cover_command(const CoverCommandRequest &msg) override;
+#endif
+#ifdef USE_FAN
+  bool send_fan_state(fan::FanState *fan);
+  bool send_fan_info(fan::FanState *fan);
+  void fan_command(const FanCommandRequest &msg) override;
+#endif
+#ifdef USE_LIGHT
+  bool send_light_state(light::LightState *light);
+  bool send_light_info(light::LightState *light);
+  void light_command(const LightCommandRequest &msg) override;
+#endif
+#ifdef USE_SENSOR
+  bool send_sensor_state(sensor::Sensor *sensor, float state);
+  bool send_sensor_info(sensor::Sensor *sensor);
+#endif
+#ifdef USE_SWITCH
+  bool send_switch_state(switch_::Switch *a_switch, bool state);
+  bool send_switch_info(switch_::Switch *a_switch);
+  void switch_command(const SwitchCommandRequest &msg) override;
+#endif
+#ifdef USE_TEXT_SENSOR
+  bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
+  bool send_text_sensor_info(text_sensor::TextSensor *text_sensor);
+#endif
+#ifdef USE_ESP32_CAMERA
+  void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
+  bool send_camera_info(esp32_camera::ESP32Camera *camera);
+  void camera_image(const CameraImageRequest &msg) override;
+#endif
+#ifdef USE_CLIMATE
+  bool send_climate_state(climate::Climate *climate);
+  bool send_climate_info(climate::Climate *climate);
+  void climate_command(const ClimateCommandRequest &msg) override;
+#endif
+  bool send_log_message(int level, const char *tag, const char *line);
+  void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
+    if (!this->service_call_subscription_)
+      return;
+    this->send_homeassistant_service_response(call);
+  }
+#ifdef USE_HOMEASSISTANT_TIME
+  void send_time_request() {
+    GetTimeRequest req;
+    this->send_get_time_request(req);
+  }
+#endif
+
+  void on_disconnect_response(const DisconnectResponse &value) override {
+    // we initiated disconnect_client
+    this->next_close_ = true;
+  }
+  void on_ping_response(const PingResponse &value) override {
+    // we initiated ping
+    this->sent_ping_ = false;
+  }
+  void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
+#ifdef USE_HOMEASSISTANT_TIME
+  void on_get_time_response(const GetTimeResponse &value) override;
+#endif
+  HelloResponse hello(const HelloRequest &msg) override;
+  ConnectResponse connect(const ConnectRequest &msg) override;
+  DisconnectResponse disconnect(const DisconnectRequest &msg) override {
+    // remote initiated disconnect_client
+    this->next_close_ = true;
+    DisconnectResponse resp;
+    return resp;
+  }
+  PingResponse ping(const PingRequest &msg) override { return {}; }
+  DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
+  void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
+  void subscribe_states(const SubscribeStatesRequest &msg) override {
+    this->state_subscription_ = true;
+    this->initial_state_iterator_.begin();
+  }
+  void subscribe_logs(const SubscribeLogsRequest &msg) override {
+    this->log_subscription_ = msg.level;
+    if (msg.dump_config)
+      App.schedule_dump_config();
+  }
+  void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
+    this->service_call_subscription_ = true;
+  }
+  void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
+  GetTimeResponse get_time(const GetTimeRequest &msg) override {
+    // TODO
+    return {};
+  }
+  void execute_service(const ExecuteServiceRequest &msg) override;
+  bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; }
+  bool is_connection_setup() override {
+    return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated();
+  }
+  void on_fatal_error() override;
+  void on_unauthenticated_access() override;
+  void on_no_setup_connection() override;
+  ProtoWriteBuffer create_buffer() override {
+    this->send_buffer_.clear();
+    return {&this->send_buffer_};
+  }
+  bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
+
+ protected:
+  friend APIServer;
+
+  void on_error_(int8_t error);
+  void on_disconnect_();
+  void on_timeout_(uint32_t time);
+  void on_data_(uint8_t *buf, size_t len);
+  void parse_recv_buffer_();
+
+  enum class ConnectionState {
+    WAITING_FOR_HELLO,
+    CONNECTED,
+    AUTHENTICATED,
+  } connection_state_{ConnectionState::WAITING_FOR_HELLO};
+
+  bool remove_{false};
+
+  std::vector<uint8_t> send_buffer_;
+  std::vector<uint8_t> recv_buffer_;
+
+  std::string client_info_;
+#ifdef USE_ESP32_CAMERA
+  esp32_camera::CameraImageReader image_reader_;
+#endif
+
+  bool state_subscription_{false};
+  int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
+  uint32_t last_traffic_;
+  bool sent_ping_{false};
+  bool service_call_subscription_{false};
+  bool current_nodelay_{false};
+  bool next_close_{false};
+  AsyncClient *client_;
+  APIServer *parent_;
+  InitialStateIterator initial_state_iterator_;
+  ListEntitiesIterator list_entities_iterator_;
+};
+
+}  // namespace api
+}  // namespace esphome

+ 2973 - 0
livingroom/src/esphome/components/api/api_pb2.cpp

@@ -0,0 +1,2973 @@
+// This file was automatically generated with a tool.
+// See scripts/api_protobuf/api_protobuf.py
+#include "api_pb2.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+template<> const char *proto_enum_to_string<enums::LegacyCoverState>(enums::LegacyCoverState value) {
+  switch (value) {
+    case enums::LEGACY_COVER_STATE_OPEN:
+      return "LEGACY_COVER_STATE_OPEN";
+    case enums::LEGACY_COVER_STATE_CLOSED:
+      return "LEGACY_COVER_STATE_CLOSED";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverOperation value) {
+  switch (value) {
+    case enums::COVER_OPERATION_IDLE:
+      return "COVER_OPERATION_IDLE";
+    case enums::COVER_OPERATION_IS_OPENING:
+      return "COVER_OPERATION_IS_OPENING";
+    case enums::COVER_OPERATION_IS_CLOSING:
+      return "COVER_OPERATION_IS_CLOSING";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::LegacyCoverCommand>(enums::LegacyCoverCommand value) {
+  switch (value) {
+    case enums::LEGACY_COVER_COMMAND_OPEN:
+      return "LEGACY_COVER_COMMAND_OPEN";
+    case enums::LEGACY_COVER_COMMAND_CLOSE:
+      return "LEGACY_COVER_COMMAND_CLOSE";
+    case enums::LEGACY_COVER_COMMAND_STOP:
+      return "LEGACY_COVER_COMMAND_STOP";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::FanSpeed>(enums::FanSpeed value) {
+  switch (value) {
+    case enums::FAN_SPEED_LOW:
+      return "FAN_SPEED_LOW";
+    case enums::FAN_SPEED_MEDIUM:
+      return "FAN_SPEED_MEDIUM";
+    case enums::FAN_SPEED_HIGH:
+      return "FAN_SPEED_HIGH";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirection value) {
+  switch (value) {
+    case enums::FAN_DIRECTION_FORWARD:
+      return "FAN_DIRECTION_FORWARD";
+    case enums::FAN_DIRECTION_REVERSE:
+      return "FAN_DIRECTION_REVERSE";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel value) {
+  switch (value) {
+    case enums::LOG_LEVEL_NONE:
+      return "LOG_LEVEL_NONE";
+    case enums::LOG_LEVEL_ERROR:
+      return "LOG_LEVEL_ERROR";
+    case enums::LOG_LEVEL_WARN:
+      return "LOG_LEVEL_WARN";
+    case enums::LOG_LEVEL_INFO:
+      return "LOG_LEVEL_INFO";
+    case enums::LOG_LEVEL_DEBUG:
+      return "LOG_LEVEL_DEBUG";
+    case enums::LOG_LEVEL_VERBOSE:
+      return "LOG_LEVEL_VERBOSE";
+    case enums::LOG_LEVEL_VERY_VERBOSE:
+      return "LOG_LEVEL_VERY_VERBOSE";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
+  switch (value) {
+    case enums::SERVICE_ARG_TYPE_BOOL:
+      return "SERVICE_ARG_TYPE_BOOL";
+    case enums::SERVICE_ARG_TYPE_INT:
+      return "SERVICE_ARG_TYPE_INT";
+    case enums::SERVICE_ARG_TYPE_FLOAT:
+      return "SERVICE_ARG_TYPE_FLOAT";
+    case enums::SERVICE_ARG_TYPE_STRING:
+      return "SERVICE_ARG_TYPE_STRING";
+    case enums::SERVICE_ARG_TYPE_BOOL_ARRAY:
+      return "SERVICE_ARG_TYPE_BOOL_ARRAY";
+    case enums::SERVICE_ARG_TYPE_INT_ARRAY:
+      return "SERVICE_ARG_TYPE_INT_ARRAY";
+    case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY:
+      return "SERVICE_ARG_TYPE_FLOAT_ARRAY";
+    case enums::SERVICE_ARG_TYPE_STRING_ARRAY:
+      return "SERVICE_ARG_TYPE_STRING_ARRAY";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
+  switch (value) {
+    case enums::CLIMATE_MODE_OFF:
+      return "CLIMATE_MODE_OFF";
+    case enums::CLIMATE_MODE_AUTO:
+      return "CLIMATE_MODE_AUTO";
+    case enums::CLIMATE_MODE_COOL:
+      return "CLIMATE_MODE_COOL";
+    case enums::CLIMATE_MODE_HEAT:
+      return "CLIMATE_MODE_HEAT";
+    case enums::CLIMATE_MODE_FAN_ONLY:
+      return "CLIMATE_MODE_FAN_ONLY";
+    case enums::CLIMATE_MODE_DRY:
+      return "CLIMATE_MODE_DRY";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::ClimateFanMode>(enums::ClimateFanMode value) {
+  switch (value) {
+    case enums::CLIMATE_FAN_ON:
+      return "CLIMATE_FAN_ON";
+    case enums::CLIMATE_FAN_OFF:
+      return "CLIMATE_FAN_OFF";
+    case enums::CLIMATE_FAN_AUTO:
+      return "CLIMATE_FAN_AUTO";
+    case enums::CLIMATE_FAN_LOW:
+      return "CLIMATE_FAN_LOW";
+    case enums::CLIMATE_FAN_MEDIUM:
+      return "CLIMATE_FAN_MEDIUM";
+    case enums::CLIMATE_FAN_HIGH:
+      return "CLIMATE_FAN_HIGH";
+    case enums::CLIMATE_FAN_MIDDLE:
+      return "CLIMATE_FAN_MIDDLE";
+    case enums::CLIMATE_FAN_FOCUS:
+      return "CLIMATE_FAN_FOCUS";
+    case enums::CLIMATE_FAN_DIFFUSE:
+      return "CLIMATE_FAN_DIFFUSE";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::ClimateSwingMode>(enums::ClimateSwingMode value) {
+  switch (value) {
+    case enums::CLIMATE_SWING_OFF:
+      return "CLIMATE_SWING_OFF";
+    case enums::CLIMATE_SWING_BOTH:
+      return "CLIMATE_SWING_BOTH";
+    case enums::CLIMATE_SWING_VERTICAL:
+      return "CLIMATE_SWING_VERTICAL";
+    case enums::CLIMATE_SWING_HORIZONTAL:
+      return "CLIMATE_SWING_HORIZONTAL";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<enums::ClimateAction>(enums::ClimateAction value) {
+  switch (value) {
+    case enums::CLIMATE_ACTION_OFF:
+      return "CLIMATE_ACTION_OFF";
+    case enums::CLIMATE_ACTION_COOLING:
+      return "CLIMATE_ACTION_COOLING";
+    case enums::CLIMATE_ACTION_HEATING:
+      return "CLIMATE_ACTION_HEATING";
+    case enums::CLIMATE_ACTION_IDLE:
+      return "CLIMATE_ACTION_IDLE";
+    case enums::CLIMATE_ACTION_DRYING:
+      return "CLIMATE_ACTION_DRYING";
+    case enums::CLIMATE_ACTION_FAN:
+      return "CLIMATE_ACTION_FAN";
+    default:
+      return "UNKNOWN";
+  }
+}
+bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->client_info = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HelloRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->client_info); }
+void HelloRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HelloRequest {\n");
+  out.append("  client_info: ");
+  out.append("'").append(this->client_info).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->api_version_major = value.as_uint32();
+      return true;
+    }
+    case 2: {
+      this->api_version_minor = value.as_uint32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool HelloResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 3: {
+      this->server_info = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HelloResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_uint32(1, this->api_version_major);
+  buffer.encode_uint32(2, this->api_version_minor);
+  buffer.encode_string(3, this->server_info);
+}
+void HelloResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HelloResponse {\n");
+  out.append("  api_version_major: ");
+  sprintf(buffer, "%u", this->api_version_major);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  api_version_minor: ");
+  sprintf(buffer, "%u", this->api_version_minor);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  server_info: ");
+  out.append("'").append(this->server_info).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->password = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->password); }
+void ConnectRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ConnectRequest {\n");
+  out.append("  password: ");
+  out.append("'").append(this->password).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->invalid_password = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); }
+void ConnectResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ConnectResponse {\n");
+  out.append("  invalid_password: ");
+  out.append(YESNO(this->invalid_password));
+  out.append("\n");
+  out.append("}");
+}
+void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {}
+void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); }
+void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {}
+void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); }
+void PingRequest::encode(ProtoWriteBuffer buffer) const {}
+void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); }
+void PingResponse::encode(ProtoWriteBuffer buffer) const {}
+void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); }
+void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {}
+void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); }
+bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->uses_password = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->has_deep_sleep = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->mac_address = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->esphome_version = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->compilation_time = value.as_string();
+      return true;
+    }
+    case 6: {
+      this->model = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_bool(1, this->uses_password);
+  buffer.encode_string(2, this->name);
+  buffer.encode_string(3, this->mac_address);
+  buffer.encode_string(4, this->esphome_version);
+  buffer.encode_string(5, this->compilation_time);
+  buffer.encode_string(6, this->model);
+  buffer.encode_bool(7, this->has_deep_sleep);
+}
+void DeviceInfoResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("DeviceInfoResponse {\n");
+  out.append("  uses_password: ");
+  out.append(YESNO(this->uses_password));
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  mac_address: ");
+  out.append("'").append(this->mac_address).append("'");
+  out.append("\n");
+
+  out.append("  esphome_version: ");
+  out.append("'").append(this->esphome_version).append("'");
+  out.append("\n");
+
+  out.append("  compilation_time: ");
+  out.append("'").append(this->compilation_time).append("'");
+  out.append("\n");
+
+  out.append("  model: ");
+  out.append("'").append(this->model).append("'");
+  out.append("\n");
+
+  out.append("  has_deep_sleep: ");
+  out.append(YESNO(this->has_deep_sleep));
+  out.append("\n");
+  out.append("}");
+}
+void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {}
+void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); }
+void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {}
+void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); }
+void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {}
+void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); }
+bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 6: {
+      this->is_status_binary_sensor = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesBinarySensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->device_class = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesBinarySensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->device_class);
+  buffer.encode_bool(6, this->is_status_binary_sensor);
+}
+void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesBinarySensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  is_status_binary_sensor: ");
+  out.append(YESNO(this->is_status_binary_sensor));
+  out.append("\n");
+  out.append("}");
+}
+bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->missing_state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool BinarySensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+  buffer.encode_bool(3, this->missing_state);
+}
+void BinarySensorStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("BinarySensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->assumed_state = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->supports_position = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->supports_tilt = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesCoverResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 8: {
+      this->device_class = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesCoverResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_bool(5, this->assumed_state);
+  buffer.encode_bool(6, this->supports_position);
+  buffer.encode_bool(7, this->supports_tilt);
+  buffer.encode_string(8, this->device_class);
+}
+void ListEntitiesCoverResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesCoverResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  assumed_state: ");
+  out.append(YESNO(this->assumed_state));
+  out.append("\n");
+
+  out.append("  supports_position: ");
+  out.append(YESNO(this->supports_position));
+  out.append("\n");
+
+  out.append("  supports_tilt: ");
+  out.append(YESNO(this->supports_tilt));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->legacy_state = value.as_enum<enums::LegacyCoverState>();
+      return true;
+    }
+    case 5: {
+      this->current_operation = value.as_enum<enums::CoverOperation>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool CoverStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 3: {
+      this->position = value.as_float();
+      return true;
+    }
+    case 4: {
+      this->tilt = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void CoverStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_enum<enums::LegacyCoverState>(2, this->legacy_state);
+  buffer.encode_float(3, this->position);
+  buffer.encode_float(4, this->tilt);
+  buffer.encode_enum<enums::CoverOperation>(5, this->current_operation);
+}
+void CoverStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("CoverStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  legacy_state: ");
+  out.append(proto_enum_to_string<enums::LegacyCoverState>(this->legacy_state));
+  out.append("\n");
+
+  out.append("  position: ");
+  sprintf(buffer, "%g", this->position);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  tilt: ");
+  sprintf(buffer, "%g", this->tilt);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  current_operation: ");
+  out.append(proto_enum_to_string<enums::CoverOperation>(this->current_operation));
+  out.append("\n");
+  out.append("}");
+}
+bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->has_legacy_command = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->legacy_command = value.as_enum<enums::LegacyCoverCommand>();
+      return true;
+    }
+    case 4: {
+      this->has_position = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->has_tilt = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->stop = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 5: {
+      this->position = value.as_float();
+      return true;
+    }
+    case 7: {
+      this->tilt = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->has_legacy_command);
+  buffer.encode_enum<enums::LegacyCoverCommand>(3, this->legacy_command);
+  buffer.encode_bool(4, this->has_position);
+  buffer.encode_float(5, this->position);
+  buffer.encode_bool(6, this->has_tilt);
+  buffer.encode_float(7, this->tilt);
+  buffer.encode_bool(8, this->stop);
+}
+void CoverCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("CoverCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_legacy_command: ");
+  out.append(YESNO(this->has_legacy_command));
+  out.append("\n");
+
+  out.append("  legacy_command: ");
+  out.append(proto_enum_to_string<enums::LegacyCoverCommand>(this->legacy_command));
+  out.append("\n");
+
+  out.append("  has_position: ");
+  out.append(YESNO(this->has_position));
+  out.append("\n");
+
+  out.append("  position: ");
+  sprintf(buffer, "%g", this->position);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_tilt: ");
+  out.append(YESNO(this->has_tilt));
+  out.append("\n");
+
+  out.append("  tilt: ");
+  sprintf(buffer, "%g", this->tilt);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  stop: ");
+  out.append(YESNO(this->stop));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->supports_oscillation = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->supports_speed = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->supports_direction = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesFanResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_bool(5, this->supports_oscillation);
+  buffer.encode_bool(6, this->supports_speed);
+  buffer.encode_bool(7, this->supports_direction);
+}
+void ListEntitiesFanResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesFanResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  supports_oscillation: ");
+  out.append(YESNO(this->supports_oscillation));
+  out.append("\n");
+
+  out.append("  supports_speed: ");
+  out.append(YESNO(this->supports_speed));
+  out.append("\n");
+
+  out.append("  supports_direction: ");
+  out.append(YESNO(this->supports_direction));
+  out.append("\n");
+  out.append("}");
+}
+bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->oscillating = value.as_bool();
+      return true;
+    }
+    case 4: {
+      this->speed = value.as_enum<enums::FanSpeed>();
+      return true;
+    }
+    case 5: {
+      this->direction = value.as_enum<enums::FanDirection>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+  buffer.encode_bool(3, this->oscillating);
+  buffer.encode_enum<enums::FanSpeed>(4, this->speed);
+  buffer.encode_enum<enums::FanDirection>(5, this->direction);
+}
+void FanStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("FanStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  oscillating: ");
+  out.append(YESNO(this->oscillating));
+  out.append("\n");
+
+  out.append("  speed: ");
+  out.append(proto_enum_to_string<enums::FanSpeed>(this->speed));
+  out.append("\n");
+
+  out.append("  direction: ");
+  out.append(proto_enum_to_string<enums::FanDirection>(this->direction));
+  out.append("\n");
+  out.append("}");
+}
+bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->has_state = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->state = value.as_bool();
+      return true;
+    }
+    case 4: {
+      this->has_speed = value.as_bool();
+      return true;
+    }
+    case 5: {
+      this->speed = value.as_enum<enums::FanSpeed>();
+      return true;
+    }
+    case 6: {
+      this->has_oscillating = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->oscillating = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->has_direction = value.as_bool();
+      return true;
+    }
+    case 9: {
+      this->direction = value.as_enum<enums::FanDirection>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->has_state);
+  buffer.encode_bool(3, this->state);
+  buffer.encode_bool(4, this->has_speed);
+  buffer.encode_enum<enums::FanSpeed>(5, this->speed);
+  buffer.encode_bool(6, this->has_oscillating);
+  buffer.encode_bool(7, this->oscillating);
+  buffer.encode_bool(8, this->has_direction);
+  buffer.encode_enum<enums::FanDirection>(9, this->direction);
+}
+void FanCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("FanCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_state: ");
+  out.append(YESNO(this->has_state));
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  has_speed: ");
+  out.append(YESNO(this->has_speed));
+  out.append("\n");
+
+  out.append("  speed: ");
+  out.append(proto_enum_to_string<enums::FanSpeed>(this->speed));
+  out.append("\n");
+
+  out.append("  has_oscillating: ");
+  out.append(YESNO(this->has_oscillating));
+  out.append("\n");
+
+  out.append("  oscillating: ");
+  out.append(YESNO(this->oscillating));
+  out.append("\n");
+
+  out.append("  has_direction: ");
+  out.append(YESNO(this->has_direction));
+  out.append("\n");
+
+  out.append("  direction: ");
+  out.append(proto_enum_to_string<enums::FanDirection>(this->direction));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->supports_brightness = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->supports_rgb = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->supports_white_value = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->supports_color_temperature = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesLightResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 11: {
+      this->effects.push_back(value.as_string());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesLightResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 9: {
+      this->min_mireds = value.as_float();
+      return true;
+    }
+    case 10: {
+      this->max_mireds = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_bool(5, this->supports_brightness);
+  buffer.encode_bool(6, this->supports_rgb);
+  buffer.encode_bool(7, this->supports_white_value);
+  buffer.encode_bool(8, this->supports_color_temperature);
+  buffer.encode_float(9, this->min_mireds);
+  buffer.encode_float(10, this->max_mireds);
+  for (auto &it : this->effects) {
+    buffer.encode_string(11, it, true);
+  }
+}
+void ListEntitiesLightResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesLightResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  supports_brightness: ");
+  out.append(YESNO(this->supports_brightness));
+  out.append("\n");
+
+  out.append("  supports_rgb: ");
+  out.append(YESNO(this->supports_rgb));
+  out.append("\n");
+
+  out.append("  supports_white_value: ");
+  out.append(YESNO(this->supports_white_value));
+  out.append("\n");
+
+  out.append("  supports_color_temperature: ");
+  out.append(YESNO(this->supports_color_temperature));
+  out.append("\n");
+
+  out.append("  min_mireds: ");
+  sprintf(buffer, "%g", this->min_mireds);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  max_mireds: ");
+  sprintf(buffer, "%g", this->max_mireds);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->effects) {
+    out.append("  effects: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool LightStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 9: {
+      this->effect = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 3: {
+      this->brightness = value.as_float();
+      return true;
+    }
+    case 4: {
+      this->red = value.as_float();
+      return true;
+    }
+    case 5: {
+      this->green = value.as_float();
+      return true;
+    }
+    case 6: {
+      this->blue = value.as_float();
+      return true;
+    }
+    case 7: {
+      this->white = value.as_float();
+      return true;
+    }
+    case 8: {
+      this->color_temperature = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void LightStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+  buffer.encode_float(3, this->brightness);
+  buffer.encode_float(4, this->red);
+  buffer.encode_float(5, this->green);
+  buffer.encode_float(6, this->blue);
+  buffer.encode_float(7, this->white);
+  buffer.encode_float(8, this->color_temperature);
+  buffer.encode_string(9, this->effect);
+}
+void LightStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("LightStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  brightness: ");
+  sprintf(buffer, "%g", this->brightness);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  red: ");
+  sprintf(buffer, "%g", this->red);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  green: ");
+  sprintf(buffer, "%g", this->green);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  blue: ");
+  sprintf(buffer, "%g", this->blue);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  white: ");
+  sprintf(buffer, "%g", this->white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  color_temperature: ");
+  sprintf(buffer, "%g", this->color_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  effect: ");
+  out.append("'").append(this->effect).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->has_state = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->state = value.as_bool();
+      return true;
+    }
+    case 4: {
+      this->has_brightness = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->has_rgb = value.as_bool();
+      return true;
+    }
+    case 10: {
+      this->has_white = value.as_bool();
+      return true;
+    }
+    case 12: {
+      this->has_color_temperature = value.as_bool();
+      return true;
+    }
+    case 14: {
+      this->has_transition_length = value.as_bool();
+      return true;
+    }
+    case 15: {
+      this->transition_length = value.as_uint32();
+      return true;
+    }
+    case 16: {
+      this->has_flash_length = value.as_bool();
+      return true;
+    }
+    case 17: {
+      this->flash_length = value.as_uint32();
+      return true;
+    }
+    case 18: {
+      this->has_effect = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 19: {
+      this->effect = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 5: {
+      this->brightness = value.as_float();
+      return true;
+    }
+    case 7: {
+      this->red = value.as_float();
+      return true;
+    }
+    case 8: {
+      this->green = value.as_float();
+      return true;
+    }
+    case 9: {
+      this->blue = value.as_float();
+      return true;
+    }
+    case 11: {
+      this->white = value.as_float();
+      return true;
+    }
+    case 13: {
+      this->color_temperature = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void LightCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->has_state);
+  buffer.encode_bool(3, this->state);
+  buffer.encode_bool(4, this->has_brightness);
+  buffer.encode_float(5, this->brightness);
+  buffer.encode_bool(6, this->has_rgb);
+  buffer.encode_float(7, this->red);
+  buffer.encode_float(8, this->green);
+  buffer.encode_float(9, this->blue);
+  buffer.encode_bool(10, this->has_white);
+  buffer.encode_float(11, this->white);
+  buffer.encode_bool(12, this->has_color_temperature);
+  buffer.encode_float(13, this->color_temperature);
+  buffer.encode_bool(14, this->has_transition_length);
+  buffer.encode_uint32(15, this->transition_length);
+  buffer.encode_bool(16, this->has_flash_length);
+  buffer.encode_uint32(17, this->flash_length);
+  buffer.encode_bool(18, this->has_effect);
+  buffer.encode_string(19, this->effect);
+}
+void LightCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("LightCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_state: ");
+  out.append(YESNO(this->has_state));
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  has_brightness: ");
+  out.append(YESNO(this->has_brightness));
+  out.append("\n");
+
+  out.append("  brightness: ");
+  sprintf(buffer, "%g", this->brightness);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_rgb: ");
+  out.append(YESNO(this->has_rgb));
+  out.append("\n");
+
+  out.append("  red: ");
+  sprintf(buffer, "%g", this->red);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  green: ");
+  sprintf(buffer, "%g", this->green);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  blue: ");
+  sprintf(buffer, "%g", this->blue);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_white: ");
+  out.append(YESNO(this->has_white));
+  out.append("\n");
+
+  out.append("  white: ");
+  sprintf(buffer, "%g", this->white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_color_temperature: ");
+  out.append(YESNO(this->has_color_temperature));
+  out.append("\n");
+
+  out.append("  color_temperature: ");
+  sprintf(buffer, "%g", this->color_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_transition_length: ");
+  out.append(YESNO(this->has_transition_length));
+  out.append("\n");
+
+  out.append("  transition_length: ");
+  sprintf(buffer, "%u", this->transition_length);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_flash_length: ");
+  out.append(YESNO(this->has_flash_length));
+  out.append("\n");
+
+  out.append("  flash_length: ");
+  sprintf(buffer, "%u", this->flash_length);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_effect: ");
+  out.append(YESNO(this->has_effect));
+  out.append("\n");
+
+  out.append("  effect: ");
+  out.append("'").append(this->effect).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 7: {
+      this->accuracy_decimals = value.as_int32();
+      return true;
+    }
+    case 8: {
+      this->force_update = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->icon = value.as_string();
+      return true;
+    }
+    case 6: {
+      this->unit_of_measurement = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesSensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->icon);
+  buffer.encode_string(6, this->unit_of_measurement);
+  buffer.encode_int32(7, this->accuracy_decimals);
+  buffer.encode_bool(8, this->force_update);
+}
+void ListEntitiesSensorResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesSensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  unit_of_measurement: ");
+  out.append("'").append(this->unit_of_measurement).append("'");
+  out.append("\n");
+
+  out.append("  accuracy_decimals: ");
+  sprintf(buffer, "%d", this->accuracy_decimals);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  force_update: ");
+  out.append(YESNO(this->force_update));
+  out.append("\n");
+  out.append("}");
+}
+bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 3: {
+      this->missing_state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool SensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 2: {
+      this->state = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SensorStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_float(2, this->state);
+  buffer.encode_bool(3, this->missing_state);
+}
+void SensorStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  sprintf(buffer, "%g", this->state);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 6: {
+      this->assumed_state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesSwitchResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->icon = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesSwitchResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->icon);
+  buffer.encode_bool(6, this->assumed_state);
+}
+void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesSwitchResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  assumed_state: ");
+  out.append(YESNO(this->assumed_state));
+  out.append("\n");
+  out.append("}");
+}
+bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool SwitchStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+}
+void SwitchStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SwitchStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+  out.append("}");
+}
+bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+}
+void SwitchCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SwitchCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->icon = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesTextSensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->icon);
+}
+void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesTextSensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 3: {
+      this->missing_state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool TextSensorStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool TextSensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_string(2, this->state);
+  buffer.encode_bool(3, this->missing_state);
+}
+void TextSensorStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("TextSensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+
+  out.append("  missing_state: ");
+  out.append(YESNO(this->missing_state));
+  out.append("\n");
+  out.append("}");
+}
+bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->level = value.as_enum<enums::LogLevel>();
+      return true;
+    }
+    case 2: {
+      this->dump_config = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_enum<enums::LogLevel>(1, this->level);
+  buffer.encode_bool(2, this->dump_config);
+}
+void SubscribeLogsRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SubscribeLogsRequest {\n");
+  out.append("  level: ");
+  out.append(proto_enum_to_string<enums::LogLevel>(this->level));
+  out.append("\n");
+
+  out.append("  dump_config: ");
+  out.append(YESNO(this->dump_config));
+  out.append("\n");
+  out.append("}");
+}
+bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->level = value.as_enum<enums::LogLevel>();
+      return true;
+    }
+    case 4: {
+      this->send_failed = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->tag = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->message = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_enum<enums::LogLevel>(1, this->level);
+  buffer.encode_string(2, this->tag);
+  buffer.encode_string(3, this->message);
+  buffer.encode_bool(4, this->send_failed);
+}
+void SubscribeLogsResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SubscribeLogsResponse {\n");
+  out.append("  level: ");
+  out.append(proto_enum_to_string<enums::LogLevel>(this->level));
+  out.append("\n");
+
+  out.append("  tag: ");
+  out.append("'").append(this->tag).append("'");
+  out.append("\n");
+
+  out.append("  message: ");
+  out.append("'").append(this->message).append("'");
+  out.append("\n");
+
+  out.append("  send_failed: ");
+  out.append(YESNO(this->send_failed));
+  out.append("\n");
+  out.append("}");
+}
+void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {}
+void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const {
+  out.append("SubscribeHomeassistantServicesRequest {}");
+}
+bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_string();
+      return true;
+    }
+    case 2: {
+      this->value = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->key);
+  buffer.encode_string(2, this->value);
+}
+void HomeassistantServiceMap::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HomeassistantServiceMap {\n");
+  out.append("  key: ");
+  out.append("'").append(this->key).append("'");
+  out.append("\n");
+
+  out.append("  value: ");
+  out.append("'").append(this->value).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool HomeassistantServiceResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->is_event = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool HomeassistantServiceResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->service = value.as_string();
+      return true;
+    }
+    case 2: {
+      this->data.push_back(value.as_message<HomeassistantServiceMap>());
+      return true;
+    }
+    case 3: {
+      this->data_template.push_back(value.as_message<HomeassistantServiceMap>());
+      return true;
+    }
+    case 4: {
+      this->variables.push_back(value.as_message<HomeassistantServiceMap>());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->service);
+  for (auto &it : this->data) {
+    buffer.encode_message<HomeassistantServiceMap>(2, it, true);
+  }
+  for (auto &it : this->data_template) {
+    buffer.encode_message<HomeassistantServiceMap>(3, it, true);
+  }
+  for (auto &it : this->variables) {
+    buffer.encode_message<HomeassistantServiceMap>(4, it, true);
+  }
+  buffer.encode_bool(5, this->is_event);
+}
+void HomeassistantServiceResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HomeassistantServiceResponse {\n");
+  out.append("  service: ");
+  out.append("'").append(this->service).append("'");
+  out.append("\n");
+
+  for (const auto &it : this->data) {
+    out.append("  data: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->data_template) {
+    out.append("  data_template: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->variables) {
+    out.append("  variables: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  out.append("  is_event: ");
+  out.append(YESNO(this->is_event));
+  out.append("\n");
+  out.append("}");
+}
+void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {}
+void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const {
+  out.append("SubscribeHomeAssistantStatesRequest {}");
+}
+bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->entity_id = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->entity_id);
+}
+void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SubscribeHomeAssistantStateResponse {\n");
+  out.append("  entity_id: ");
+  out.append("'").append(this->entity_id).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->entity_id = value.as_string();
+      return true;
+    }
+    case 2: {
+      this->state = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->entity_id);
+  buffer.encode_string(2, this->state);
+}
+void HomeAssistantStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HomeAssistantStateResponse {\n");
+  out.append("  entity_id: ");
+  out.append("'").append(this->entity_id).append("'");
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {}
+void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
+bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->epoch_seconds = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); }
+void GetTimeResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("GetTimeResponse {\n");
+  out.append("  epoch_seconds: ");
+  sprintf(buffer, "%u", this->epoch_seconds);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->type = value.as_enum<enums::ServiceArgType>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesServicesArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->name = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->name);
+  buffer.encode_enum<enums::ServiceArgType>(2, this->type);
+}
+void ListEntitiesServicesArgument::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesServicesArgument {\n");
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  type: ");
+  out.append(proto_enum_to_string<enums::ServiceArgType>(this->type));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->args.push_back(value.as_message<ListEntitiesServicesArgument>());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesServicesResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->name);
+  buffer.encode_fixed32(2, this->key);
+  for (auto &it : this->args) {
+    buffer.encode_message<ListEntitiesServicesArgument>(3, it, true);
+  }
+}
+void ListEntitiesServicesResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesServicesResponse {\n");
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->args) {
+    out.append("  args: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->bool_ = value.as_bool();
+      return true;
+    }
+    case 2: {
+      this->legacy_int = value.as_int32();
+      return true;
+    }
+    case 5: {
+      this->int_ = value.as_sint32();
+      return true;
+    }
+    case 6: {
+      this->bool_array.push_back(value.as_bool());
+      return true;
+    }
+    case 7: {
+      this->int_array.push_back(value.as_sint32());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 4: {
+      this->string_ = value.as_string();
+      return true;
+    }
+    case 9: {
+      this->string_array.push_back(value.as_string());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 3: {
+      this->float_ = value.as_float();
+      return true;
+    }
+    case 8: {
+      this->float_array.push_back(value.as_float());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_bool(1, this->bool_);
+  buffer.encode_int32(2, this->legacy_int);
+  buffer.encode_float(3, this->float_);
+  buffer.encode_string(4, this->string_);
+  buffer.encode_sint32(5, this->int_);
+  for (auto it : this->bool_array) {
+    buffer.encode_bool(6, it, true);
+  }
+  for (auto &it : this->int_array) {
+    buffer.encode_sint32(7, it, true);
+  }
+  for (auto &it : this->float_array) {
+    buffer.encode_float(8, it, true);
+  }
+  for (auto &it : this->string_array) {
+    buffer.encode_string(9, it, true);
+  }
+}
+void ExecuteServiceArgument::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ExecuteServiceArgument {\n");
+  out.append("  bool_: ");
+  out.append(YESNO(this->bool_));
+  out.append("\n");
+
+  out.append("  legacy_int: ");
+  sprintf(buffer, "%d", this->legacy_int);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  float_: ");
+  sprintf(buffer, "%g", this->float_);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  string_: ");
+  out.append("'").append(this->string_).append("'");
+  out.append("\n");
+
+  out.append("  int_: ");
+  sprintf(buffer, "%d", this->int_);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto it : this->bool_array) {
+    out.append("  bool_array: ");
+    out.append(YESNO(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->int_array) {
+    out.append("  int_array: ");
+    sprintf(buffer, "%d", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->float_array) {
+    out.append("  float_array: ");
+    sprintf(buffer, "%g", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->string_array) {
+    out.append("  string_array: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->args.push_back(value.as_message<ExecuteServiceArgument>());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ExecuteServiceRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  for (auto &it : this->args) {
+    buffer.encode_message<ExecuteServiceArgument>(2, it, true);
+  }
+}
+void ExecuteServiceRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ExecuteServiceRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->args) {
+    out.append("  args: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesCameraResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+}
+void ListEntitiesCameraResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesCameraResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 3: {
+      this->done = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool CameraImageResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->data = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool CameraImageResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void CameraImageResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_string(2, this->data);
+  buffer.encode_bool(3, this->done);
+}
+void CameraImageResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("CameraImageResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  data: ");
+  out.append("'").append(this->data).append("'");
+  out.append("\n");
+
+  out.append("  done: ");
+  out.append(YESNO(this->done));
+  out.append("\n");
+  out.append("}");
+}
+bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->single = value.as_bool();
+      return true;
+    }
+    case 2: {
+      this->stream = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void CameraImageRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_bool(1, this->single);
+  buffer.encode_bool(2, this->stream);
+}
+void CameraImageRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("CameraImageRequest {\n");
+  out.append("  single: ");
+  out.append(YESNO(this->single));
+  out.append("\n");
+
+  out.append("  stream: ");
+  out.append(YESNO(this->stream));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->supports_current_temperature = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->supports_two_point_target_temperature = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->supported_modes.push_back(value.as_enum<enums::ClimateMode>());
+      return true;
+    }
+    case 11: {
+      this->supports_away = value.as_bool();
+      return true;
+    }
+    case 12: {
+      this->supports_action = value.as_bool();
+      return true;
+    }
+    case 13: {
+      this->supported_fan_modes.push_back(value.as_enum<enums::ClimateFanMode>());
+      return true;
+    }
+    case 14: {
+      this->supported_swing_modes.push_back(value.as_enum<enums::ClimateSwingMode>());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 8: {
+      this->visual_min_temperature = value.as_float();
+      return true;
+    }
+    case 9: {
+      this->visual_max_temperature = value.as_float();
+      return true;
+    }
+    case 10: {
+      this->visual_temperature_step = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_bool(5, this->supports_current_temperature);
+  buffer.encode_bool(6, this->supports_two_point_target_temperature);
+  for (auto &it : this->supported_modes) {
+    buffer.encode_enum<enums::ClimateMode>(7, it, true);
+  }
+  buffer.encode_float(8, this->visual_min_temperature);
+  buffer.encode_float(9, this->visual_max_temperature);
+  buffer.encode_float(10, this->visual_temperature_step);
+  buffer.encode_bool(11, this->supports_away);
+  buffer.encode_bool(12, this->supports_action);
+  for (auto &it : this->supported_fan_modes) {
+    buffer.encode_enum<enums::ClimateFanMode>(13, it, true);
+  }
+  for (auto &it : this->supported_swing_modes) {
+    buffer.encode_enum<enums::ClimateSwingMode>(14, it, true);
+  }
+}
+void ListEntitiesClimateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesClimateResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  supports_current_temperature: ");
+  out.append(YESNO(this->supports_current_temperature));
+  out.append("\n");
+
+  out.append("  supports_two_point_target_temperature: ");
+  out.append(YESNO(this->supports_two_point_target_temperature));
+  out.append("\n");
+
+  for (const auto &it : this->supported_modes) {
+    out.append("  supported_modes: ");
+    out.append(proto_enum_to_string<enums::ClimateMode>(it));
+    out.append("\n");
+  }
+
+  out.append("  visual_min_temperature: ");
+  sprintf(buffer, "%g", this->visual_min_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  visual_max_temperature: ");
+  sprintf(buffer, "%g", this->visual_max_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  visual_temperature_step: ");
+  sprintf(buffer, "%g", this->visual_temperature_step);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  supports_away: ");
+  out.append(YESNO(this->supports_away));
+  out.append("\n");
+
+  out.append("  supports_action: ");
+  out.append(YESNO(this->supports_action));
+  out.append("\n");
+
+  for (const auto &it : this->supported_fan_modes) {
+    out.append("  supported_fan_modes: ");
+    out.append(proto_enum_to_string<enums::ClimateFanMode>(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_swing_modes) {
+    out.append("  supported_swing_modes: ");
+    out.append(proto_enum_to_string<enums::ClimateSwingMode>(it));
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->mode = value.as_enum<enums::ClimateMode>();
+      return true;
+    }
+    case 7: {
+      this->away = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->action = value.as_enum<enums::ClimateAction>();
+      return true;
+    }
+    case 9: {
+      this->fan_mode = value.as_enum<enums::ClimateFanMode>();
+      return true;
+    }
+    case 10: {
+      this->swing_mode = value.as_enum<enums::ClimateSwingMode>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 3: {
+      this->current_temperature = value.as_float();
+      return true;
+    }
+    case 4: {
+      this->target_temperature = value.as_float();
+      return true;
+    }
+    case 5: {
+      this->target_temperature_low = value.as_float();
+      return true;
+    }
+    case 6: {
+      this->target_temperature_high = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_enum<enums::ClimateMode>(2, this->mode);
+  buffer.encode_float(3, this->current_temperature);
+  buffer.encode_float(4, this->target_temperature);
+  buffer.encode_float(5, this->target_temperature_low);
+  buffer.encode_float(6, this->target_temperature_high);
+  buffer.encode_bool(7, this->away);
+  buffer.encode_enum<enums::ClimateAction>(8, this->action);
+  buffer.encode_enum<enums::ClimateFanMode>(9, this->fan_mode);
+  buffer.encode_enum<enums::ClimateSwingMode>(10, this->swing_mode);
+}
+void ClimateStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ClimateStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string<enums::ClimateMode>(this->mode));
+  out.append("\n");
+
+  out.append("  current_temperature: ");
+  sprintf(buffer, "%g", this->current_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature: ");
+  sprintf(buffer, "%g", this->target_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature_low: ");
+  sprintf(buffer, "%g", this->target_temperature_low);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature_high: ");
+  sprintf(buffer, "%g", this->target_temperature_high);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  away: ");
+  out.append(YESNO(this->away));
+  out.append("\n");
+
+  out.append("  action: ");
+  out.append(proto_enum_to_string<enums::ClimateAction>(this->action));
+  out.append("\n");
+
+  out.append("  fan_mode: ");
+  out.append(proto_enum_to_string<enums::ClimateFanMode>(this->fan_mode));
+  out.append("\n");
+
+  out.append("  swing_mode: ");
+  out.append(proto_enum_to_string<enums::ClimateSwingMode>(this->swing_mode));
+  out.append("\n");
+  out.append("}");
+}
+bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->has_mode = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->mode = value.as_enum<enums::ClimateMode>();
+      return true;
+    }
+    case 4: {
+      this->has_target_temperature = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->has_target_temperature_low = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->has_target_temperature_high = value.as_bool();
+      return true;
+    }
+    case 10: {
+      this->has_away = value.as_bool();
+      return true;
+    }
+    case 11: {
+      this->away = value.as_bool();
+      return true;
+    }
+    case 12: {
+      this->has_fan_mode = value.as_bool();
+      return true;
+    }
+    case 13: {
+      this->fan_mode = value.as_enum<enums::ClimateFanMode>();
+      return true;
+    }
+    case 14: {
+      this->has_swing_mode = value.as_bool();
+      return true;
+    }
+    case 15: {
+      this->swing_mode = value.as_enum<enums::ClimateSwingMode>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 5: {
+      this->target_temperature = value.as_float();
+      return true;
+    }
+    case 7: {
+      this->target_temperature_low = value.as_float();
+      return true;
+    }
+    case 9: {
+      this->target_temperature_high = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->has_mode);
+  buffer.encode_enum<enums::ClimateMode>(3, this->mode);
+  buffer.encode_bool(4, this->has_target_temperature);
+  buffer.encode_float(5, this->target_temperature);
+  buffer.encode_bool(6, this->has_target_temperature_low);
+  buffer.encode_float(7, this->target_temperature_low);
+  buffer.encode_bool(8, this->has_target_temperature_high);
+  buffer.encode_float(9, this->target_temperature_high);
+  buffer.encode_bool(10, this->has_away);
+  buffer.encode_bool(11, this->away);
+  buffer.encode_bool(12, this->has_fan_mode);
+  buffer.encode_enum<enums::ClimateFanMode>(13, this->fan_mode);
+  buffer.encode_bool(14, this->has_swing_mode);
+  buffer.encode_enum<enums::ClimateSwingMode>(15, this->swing_mode);
+}
+void ClimateCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ClimateCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_mode: ");
+  out.append(YESNO(this->has_mode));
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string<enums::ClimateMode>(this->mode));
+  out.append("\n");
+
+  out.append("  has_target_temperature: ");
+  out.append(YESNO(this->has_target_temperature));
+  out.append("\n");
+
+  out.append("  target_temperature: ");
+  sprintf(buffer, "%g", this->target_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_target_temperature_low: ");
+  out.append(YESNO(this->has_target_temperature_low));
+  out.append("\n");
+
+  out.append("  target_temperature_low: ");
+  sprintf(buffer, "%g", this->target_temperature_low);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_target_temperature_high: ");
+  out.append(YESNO(this->has_target_temperature_high));
+  out.append("\n");
+
+  out.append("  target_temperature_high: ");
+  sprintf(buffer, "%g", this->target_temperature_high);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_away: ");
+  out.append(YESNO(this->has_away));
+  out.append("\n");
+
+  out.append("  away: ");
+  out.append(YESNO(this->away));
+  out.append("\n");
+
+  out.append("  has_fan_mode: ");
+  out.append(YESNO(this->has_fan_mode));
+  out.append("\n");
+
+  out.append("  fan_mode: ");
+  out.append(proto_enum_to_string<enums::ClimateFanMode>(this->fan_mode));
+  out.append("\n");
+
+  out.append("  has_swing_mode: ");
+  out.append(YESNO(this->has_swing_mode));
+  out.append("\n");
+
+  out.append("  swing_mode: ");
+  out.append(proto_enum_to_string<enums::ClimateSwingMode>(this->swing_mode));
+  out.append("\n");
+  out.append("}");
+}
+
+}  // namespace api
+}  // namespace esphome

+ 745 - 0
livingroom/src/esphome/components/api/api_pb2.h

@@ -0,0 +1,745 @@
+// This file was automatically generated with a tool.
+// See scripts/api_protobuf/api_protobuf.py
+#pragma once
+
+#include "proto.h"
+
+namespace esphome {
+namespace api {
+
+namespace enums {
+
+enum LegacyCoverState : uint32_t {
+  LEGACY_COVER_STATE_OPEN = 0,
+  LEGACY_COVER_STATE_CLOSED = 1,
+};
+enum CoverOperation : uint32_t {
+  COVER_OPERATION_IDLE = 0,
+  COVER_OPERATION_IS_OPENING = 1,
+  COVER_OPERATION_IS_CLOSING = 2,
+};
+enum LegacyCoverCommand : uint32_t {
+  LEGACY_COVER_COMMAND_OPEN = 0,
+  LEGACY_COVER_COMMAND_CLOSE = 1,
+  LEGACY_COVER_COMMAND_STOP = 2,
+};
+enum FanSpeed : uint32_t {
+  FAN_SPEED_LOW = 0,
+  FAN_SPEED_MEDIUM = 1,
+  FAN_SPEED_HIGH = 2,
+};
+enum FanDirection : uint32_t {
+  FAN_DIRECTION_FORWARD = 0,
+  FAN_DIRECTION_REVERSE = 1,
+};
+enum LogLevel : uint32_t {
+  LOG_LEVEL_NONE = 0,
+  LOG_LEVEL_ERROR = 1,
+  LOG_LEVEL_WARN = 2,
+  LOG_LEVEL_INFO = 3,
+  LOG_LEVEL_DEBUG = 4,
+  LOG_LEVEL_VERBOSE = 5,
+  LOG_LEVEL_VERY_VERBOSE = 6,
+};
+enum ServiceArgType : uint32_t {
+  SERVICE_ARG_TYPE_BOOL = 0,
+  SERVICE_ARG_TYPE_INT = 1,
+  SERVICE_ARG_TYPE_FLOAT = 2,
+  SERVICE_ARG_TYPE_STRING = 3,
+  SERVICE_ARG_TYPE_BOOL_ARRAY = 4,
+  SERVICE_ARG_TYPE_INT_ARRAY = 5,
+  SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
+  SERVICE_ARG_TYPE_STRING_ARRAY = 7,
+};
+enum ClimateMode : uint32_t {
+  CLIMATE_MODE_OFF = 0,
+  CLIMATE_MODE_AUTO = 1,
+  CLIMATE_MODE_COOL = 2,
+  CLIMATE_MODE_HEAT = 3,
+  CLIMATE_MODE_FAN_ONLY = 4,
+  CLIMATE_MODE_DRY = 5,
+};
+enum ClimateFanMode : uint32_t {
+  CLIMATE_FAN_ON = 0,
+  CLIMATE_FAN_OFF = 1,
+  CLIMATE_FAN_AUTO = 2,
+  CLIMATE_FAN_LOW = 3,
+  CLIMATE_FAN_MEDIUM = 4,
+  CLIMATE_FAN_HIGH = 5,
+  CLIMATE_FAN_MIDDLE = 6,
+  CLIMATE_FAN_FOCUS = 7,
+  CLIMATE_FAN_DIFFUSE = 8,
+};
+enum ClimateSwingMode : uint32_t {
+  CLIMATE_SWING_OFF = 0,
+  CLIMATE_SWING_BOTH = 1,
+  CLIMATE_SWING_VERTICAL = 2,
+  CLIMATE_SWING_HORIZONTAL = 3,
+};
+enum ClimateAction : uint32_t {
+  CLIMATE_ACTION_OFF = 0,
+  CLIMATE_ACTION_COOLING = 2,
+  CLIMATE_ACTION_HEATING = 3,
+  CLIMATE_ACTION_IDLE = 4,
+  CLIMATE_ACTION_DRYING = 5,
+  CLIMATE_ACTION_FAN = 6,
+};
+
+}  // namespace enums
+
+class HelloRequest : public ProtoMessage {
+ public:
+  std::string client_info{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class HelloResponse : public ProtoMessage {
+ public:
+  uint32_t api_version_major{0};  // NOLINT
+  uint32_t api_version_minor{0};  // NOLINT
+  std::string server_info{};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ConnectRequest : public ProtoMessage {
+ public:
+  std::string password{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class ConnectResponse : public ProtoMessage {
+ public:
+  bool invalid_password{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class DisconnectRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class DisconnectResponse : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class PingRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class PingResponse : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class DeviceInfoRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class DeviceInfoResponse : public ProtoMessage {
+ public:
+  bool uses_password{false};       // NOLINT
+  std::string name{};              // NOLINT
+  std::string mac_address{};       // NOLINT
+  std::string esphome_version{};   // NOLINT
+  std::string compilation_time{};  // NOLINT
+  std::string model{};             // NOLINT
+  bool has_deep_sleep{false};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class ListEntitiesDoneResponse : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class SubscribeStatesRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class ListEntitiesBinarySensorResponse : public ProtoMessage {
+ public:
+  std::string object_id{};              // NOLINT
+  uint32_t key{0};                      // NOLINT
+  std::string name{};                   // NOLINT
+  std::string unique_id{};              // NOLINT
+  std::string device_class{};           // NOLINT
+  bool is_status_binary_sensor{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class BinarySensorStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};            // NOLINT
+  bool state{false};          // NOLINT
+  bool missing_state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesCoverResponse : public ProtoMessage {
+ public:
+  std::string object_id{};        // NOLINT
+  uint32_t key{0};                // NOLINT
+  std::string name{};             // NOLINT
+  std::string unique_id{};        // NOLINT
+  bool assumed_state{false};      // NOLINT
+  bool supports_position{false};  // NOLINT
+  bool supports_tilt{false};      // NOLINT
+  std::string device_class{};     // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class CoverStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};                            // NOLINT
+  enums::LegacyCoverState legacy_state{};     // NOLINT
+  float position{0.0f};                       // NOLINT
+  float tilt{0.0f};                           // NOLINT
+  enums::CoverOperation current_operation{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class CoverCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                             // NOLINT
+  bool has_legacy_command{false};              // NOLINT
+  enums::LegacyCoverCommand legacy_command{};  // NOLINT
+  bool has_position{false};                    // NOLINT
+  float position{0.0f};                        // NOLINT
+  bool has_tilt{false};                        // NOLINT
+  float tilt{0.0f};                            // NOLINT
+  bool stop{false};                            // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesFanResponse : public ProtoMessage {
+ public:
+  std::string object_id{};           // NOLINT
+  uint32_t key{0};                   // NOLINT
+  std::string name{};                // NOLINT
+  std::string unique_id{};           // NOLINT
+  bool supports_oscillation{false};  // NOLINT
+  bool supports_speed{false};        // NOLINT
+  bool supports_direction{false};    // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class FanStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};                  // NOLINT
+  bool state{false};                // NOLINT
+  bool oscillating{false};          // NOLINT
+  enums::FanSpeed speed{};          // NOLINT
+  enums::FanDirection direction{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class FanCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                  // NOLINT
+  bool has_state{false};            // NOLINT
+  bool state{false};                // NOLINT
+  bool has_speed{false};            // NOLINT
+  enums::FanSpeed speed{};          // NOLINT
+  bool has_oscillating{false};      // NOLINT
+  bool oscillating{false};          // NOLINT
+  bool has_direction{false};        // NOLINT
+  enums::FanDirection direction{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesLightResponse : public ProtoMessage {
+ public:
+  std::string object_id{};                 // NOLINT
+  uint32_t key{0};                         // NOLINT
+  std::string name{};                      // NOLINT
+  std::string unique_id{};                 // NOLINT
+  bool supports_brightness{false};         // NOLINT
+  bool supports_rgb{false};                // NOLINT
+  bool supports_white_value{false};        // NOLINT
+  bool supports_color_temperature{false};  // NOLINT
+  float min_mireds{0.0f};                  // NOLINT
+  float max_mireds{0.0f};                  // NOLINT
+  std::vector<std::string> effects{};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class LightStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};                // NOLINT
+  bool state{false};              // NOLINT
+  float brightness{0.0f};         // NOLINT
+  float red{0.0f};                // NOLINT
+  float green{0.0f};              // NOLINT
+  float blue{0.0f};               // NOLINT
+  float white{0.0f};              // NOLINT
+  float color_temperature{0.0f};  // NOLINT
+  std::string effect{};           // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class LightCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                    // NOLINT
+  bool has_state{false};              // NOLINT
+  bool state{false};                  // NOLINT
+  bool has_brightness{false};         // NOLINT
+  float brightness{0.0f};             // NOLINT
+  bool has_rgb{false};                // NOLINT
+  float red{0.0f};                    // NOLINT
+  float green{0.0f};                  // NOLINT
+  float blue{0.0f};                   // NOLINT
+  bool has_white{false};              // NOLINT
+  float white{0.0f};                  // NOLINT
+  bool has_color_temperature{false};  // NOLINT
+  float color_temperature{0.0f};      // NOLINT
+  bool has_transition_length{false};  // NOLINT
+  uint32_t transition_length{0};      // NOLINT
+  bool has_flash_length{false};       // NOLINT
+  uint32_t flash_length{0};           // NOLINT
+  bool has_effect{false};             // NOLINT
+  std::string effect{};               // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesSensorResponse : public ProtoMessage {
+ public:
+  std::string object_id{};            // NOLINT
+  uint32_t key{0};                    // NOLINT
+  std::string name{};                 // NOLINT
+  std::string unique_id{};            // NOLINT
+  std::string icon{};                 // NOLINT
+  std::string unit_of_measurement{};  // NOLINT
+  int32_t accuracy_decimals{0};       // NOLINT
+  bool force_update{false};           // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SensorStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};            // NOLINT
+  float state{0.0f};          // NOLINT
+  bool missing_state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesSwitchResponse : public ProtoMessage {
+ public:
+  std::string object_id{};    // NOLINT
+  uint32_t key{0};            // NOLINT
+  std::string name{};         // NOLINT
+  std::string unique_id{};    // NOLINT
+  std::string icon{};         // NOLINT
+  bool assumed_state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SwitchStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};    // NOLINT
+  bool state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SwitchCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};    // NOLINT
+  bool state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesTextSensorResponse : public ProtoMessage {
+ public:
+  std::string object_id{};  // NOLINT
+  uint32_t key{0};          // NOLINT
+  std::string name{};       // NOLINT
+  std::string unique_id{};  // NOLINT
+  std::string icon{};       // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class TextSensorStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};            // NOLINT
+  std::string state{};        // NOLINT
+  bool missing_state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SubscribeLogsRequest : public ProtoMessage {
+ public:
+  enums::LogLevel level{};  // NOLINT
+  bool dump_config{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SubscribeLogsResponse : public ProtoMessage {
+ public:
+  enums::LogLevel level{};  // NOLINT
+  std::string tag{};        // NOLINT
+  std::string message{};    // NOLINT
+  bool send_failed{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SubscribeHomeassistantServicesRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class HomeassistantServiceMap : public ProtoMessage {
+ public:
+  std::string key{};    // NOLINT
+  std::string value{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class HomeassistantServiceResponse : public ProtoMessage {
+ public:
+  std::string service{};                                 // NOLINT
+  std::vector<HomeassistantServiceMap> data{};           // NOLINT
+  std::vector<HomeassistantServiceMap> data_template{};  // NOLINT
+  std::vector<HomeassistantServiceMap> variables{};      // NOLINT
+  bool is_event{false};                                  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SubscribeHomeAssistantStatesRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class SubscribeHomeAssistantStateResponse : public ProtoMessage {
+ public:
+  std::string entity_id{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class HomeAssistantStateResponse : public ProtoMessage {
+ public:
+  std::string entity_id{};  // NOLINT
+  std::string state{};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class GetTimeRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class GetTimeResponse : public ProtoMessage {
+ public:
+  uint32_t epoch_seconds{0};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+};
+class ListEntitiesServicesArgument : public ProtoMessage {
+ public:
+  std::string name{};            // NOLINT
+  enums::ServiceArgType type{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesServicesResponse : public ProtoMessage {
+ public:
+  std::string name{};                                // NOLINT
+  uint32_t key{0};                                   // NOLINT
+  std::vector<ListEntitiesServicesArgument> args{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class ExecuteServiceArgument : public ProtoMessage {
+ public:
+  bool bool_{false};                        // NOLINT
+  int32_t legacy_int{0};                    // NOLINT
+  float float_{0.0f};                       // NOLINT
+  std::string string_{};                    // NOLINT
+  int32_t int_{0};                          // NOLINT
+  std::vector<bool> bool_array{};           // NOLINT
+  std::vector<int32_t> int_array{};         // NOLINT
+  std::vector<float> float_array{};         // NOLINT
+  std::vector<std::string> string_array{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ExecuteServiceRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                             // NOLINT
+  std::vector<ExecuteServiceArgument> args{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class ListEntitiesCameraResponse : public ProtoMessage {
+ public:
+  std::string object_id{};  // NOLINT
+  uint32_t key{0};          // NOLINT
+  std::string name{};       // NOLINT
+  std::string unique_id{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class CameraImageResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};     // NOLINT
+  std::string data{};  // NOLINT
+  bool done{false};    // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class CameraImageRequest : public ProtoMessage {
+ public:
+  bool single{false};  // NOLINT
+  bool stream{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesClimateResponse : public ProtoMessage {
+ public:
+  std::string object_id{};                                       // NOLINT
+  uint32_t key{0};                                               // NOLINT
+  std::string name{};                                            // NOLINT
+  std::string unique_id{};                                       // NOLINT
+  bool supports_current_temperature{false};                      // NOLINT
+  bool supports_two_point_target_temperature{false};             // NOLINT
+  std::vector<enums::ClimateMode> supported_modes{};             // NOLINT
+  float visual_min_temperature{0.0f};                            // NOLINT
+  float visual_max_temperature{0.0f};                            // NOLINT
+  float visual_temperature_step{0.0f};                           // NOLINT
+  bool supports_away{false};                                     // NOLINT
+  bool supports_action{false};                                   // NOLINT
+  std::vector<enums::ClimateFanMode> supported_fan_modes{};      // NOLINT
+  std::vector<enums::ClimateSwingMode> supported_swing_modes{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ClimateStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};                       // NOLINT
+  enums::ClimateMode mode{};             // NOLINT
+  float current_temperature{0.0f};       // NOLINT
+  float target_temperature{0.0f};        // NOLINT
+  float target_temperature_low{0.0f};    // NOLINT
+  float target_temperature_high{0.0f};   // NOLINT
+  bool away{false};                      // NOLINT
+  enums::ClimateAction action{};         // NOLINT
+  enums::ClimateFanMode fan_mode{};      // NOLINT
+  enums::ClimateSwingMode swing_mode{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ClimateCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                          // NOLINT
+  bool has_mode{false};                     // NOLINT
+  enums::ClimateMode mode{};                // NOLINT
+  bool has_target_temperature{false};       // NOLINT
+  float target_temperature{0.0f};           // NOLINT
+  bool has_target_temperature_low{false};   // NOLINT
+  float target_temperature_low{0.0f};       // NOLINT
+  bool has_target_temperature_high{false};  // NOLINT
+  float target_temperature_high{0.0f};      // NOLINT
+  bool has_away{false};                     // NOLINT
+  bool away{false};                         // NOLINT
+  bool has_fan_mode{false};                 // NOLINT
+  enums::ClimateFanMode fan_mode{};         // NOLINT
+  bool has_swing_mode{false};               // NOLINT
+  enums::ClimateSwingMode swing_mode{};     // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+
+}  // namespace api
+}  // namespace esphome

+ 552 - 0
livingroom/src/esphome/components/api/api_pb2_service.cpp

@@ -0,0 +1,552 @@
+// This file was automatically generated with a tool.
+// See scripts/api_protobuf/api_protobuf.py
+#include "api_pb2_service.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+static const char *TAG = "api.service";
+
+bool APIServerConnectionBase::send_hello_response(const HelloResponse &msg) {
+  ESP_LOGVV(TAG, "send_hello_response: %s", msg.dump().c_str());
+  return this->send_message_<HelloResponse>(msg, 2);
+}
+bool APIServerConnectionBase::send_connect_response(const ConnectResponse &msg) {
+  ESP_LOGVV(TAG, "send_connect_response: %s", msg.dump().c_str());
+  return this->send_message_<ConnectResponse>(msg, 4);
+}
+bool APIServerConnectionBase::send_disconnect_request(const DisconnectRequest &msg) {
+  ESP_LOGVV(TAG, "send_disconnect_request: %s", msg.dump().c_str());
+  return this->send_message_<DisconnectRequest>(msg, 5);
+}
+bool APIServerConnectionBase::send_disconnect_response(const DisconnectResponse &msg) {
+  ESP_LOGVV(TAG, "send_disconnect_response: %s", msg.dump().c_str());
+  return this->send_message_<DisconnectResponse>(msg, 6);
+}
+bool APIServerConnectionBase::send_ping_request(const PingRequest &msg) {
+  ESP_LOGVV(TAG, "send_ping_request: %s", msg.dump().c_str());
+  return this->send_message_<PingRequest>(msg, 7);
+}
+bool APIServerConnectionBase::send_ping_response(const PingResponse &msg) {
+  ESP_LOGVV(TAG, "send_ping_response: %s", msg.dump().c_str());
+  return this->send_message_<PingResponse>(msg, 8);
+}
+bool APIServerConnectionBase::send_device_info_response(const DeviceInfoResponse &msg) {
+  ESP_LOGVV(TAG, "send_device_info_response: %s", msg.dump().c_str());
+  return this->send_message_<DeviceInfoResponse>(msg, 10);
+}
+bool APIServerConnectionBase::send_list_entities_done_response(const ListEntitiesDoneResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_done_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesDoneResponse>(msg, 19);
+}
+#ifdef USE_BINARY_SENSOR
+bool APIServerConnectionBase::send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_binary_sensor_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesBinarySensorResponse>(msg, 12);
+}
+#endif
+#ifdef USE_BINARY_SENSOR
+bool APIServerConnectionBase::send_binary_sensor_state_response(const BinarySensorStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_binary_sensor_state_response: %s", msg.dump().c_str());
+  return this->send_message_<BinarySensorStateResponse>(msg, 21);
+}
+#endif
+#ifdef USE_COVER
+bool APIServerConnectionBase::send_list_entities_cover_response(const ListEntitiesCoverResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_cover_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesCoverResponse>(msg, 13);
+}
+#endif
+#ifdef USE_COVER
+bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_cover_state_response: %s", msg.dump().c_str());
+  return this->send_message_<CoverStateResponse>(msg, 22);
+}
+#endif
+#ifdef USE_COVER
+#endif
+#ifdef USE_FAN
+bool APIServerConnectionBase::send_list_entities_fan_response(const ListEntitiesFanResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_fan_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesFanResponse>(msg, 14);
+}
+#endif
+#ifdef USE_FAN
+bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_fan_state_response: %s", msg.dump().c_str());
+  return this->send_message_<FanStateResponse>(msg, 23);
+}
+#endif
+#ifdef USE_FAN
+#endif
+#ifdef USE_LIGHT
+bool APIServerConnectionBase::send_list_entities_light_response(const ListEntitiesLightResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_light_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesLightResponse>(msg, 15);
+}
+#endif
+#ifdef USE_LIGHT
+bool APIServerConnectionBase::send_light_state_response(const LightStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_light_state_response: %s", msg.dump().c_str());
+  return this->send_message_<LightStateResponse>(msg, 24);
+}
+#endif
+#ifdef USE_LIGHT
+#endif
+#ifdef USE_SENSOR
+bool APIServerConnectionBase::send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_sensor_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesSensorResponse>(msg, 16);
+}
+#endif
+#ifdef USE_SENSOR
+bool APIServerConnectionBase::send_sensor_state_response(const SensorStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_sensor_state_response: %s", msg.dump().c_str());
+  return this->send_message_<SensorStateResponse>(msg, 25);
+}
+#endif
+#ifdef USE_SWITCH
+bool APIServerConnectionBase::send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_switch_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesSwitchResponse>(msg, 17);
+}
+#endif
+#ifdef USE_SWITCH
+bool APIServerConnectionBase::send_switch_state_response(const SwitchStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_switch_state_response: %s", msg.dump().c_str());
+  return this->send_message_<SwitchStateResponse>(msg, 26);
+}
+#endif
+#ifdef USE_SWITCH
+#endif
+#ifdef USE_TEXT_SENSOR
+bool APIServerConnectionBase::send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_text_sensor_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesTextSensorResponse>(msg, 18);
+}
+#endif
+#ifdef USE_TEXT_SENSOR
+bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_text_sensor_state_response: %s", msg.dump().c_str());
+  return this->send_message_<TextSensorStateResponse>(msg, 27);
+}
+#endif
+bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) {
+  return this->send_message_<SubscribeLogsResponse>(msg, 29);
+}
+bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) {
+  ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str());
+  return this->send_message_<HomeassistantServiceResponse>(msg, 35);
+}
+bool APIServerConnectionBase::send_subscribe_home_assistant_state_response(
+    const SubscribeHomeAssistantStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_subscribe_home_assistant_state_response: %s", msg.dump().c_str());
+  return this->send_message_<SubscribeHomeAssistantStateResponse>(msg, 39);
+}
+bool APIServerConnectionBase::send_get_time_request(const GetTimeRequest &msg) {
+  ESP_LOGVV(TAG, "send_get_time_request: %s", msg.dump().c_str());
+  return this->send_message_<GetTimeRequest>(msg, 36);
+}
+bool APIServerConnectionBase::send_get_time_response(const GetTimeResponse &msg) {
+  ESP_LOGVV(TAG, "send_get_time_response: %s", msg.dump().c_str());
+  return this->send_message_<GetTimeResponse>(msg, 37);
+}
+bool APIServerConnectionBase::send_list_entities_services_response(const ListEntitiesServicesResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_services_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesServicesResponse>(msg, 41);
+}
+#ifdef USE_ESP32_CAMERA
+bool APIServerConnectionBase::send_list_entities_camera_response(const ListEntitiesCameraResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_camera_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesCameraResponse>(msg, 43);
+}
+#endif
+#ifdef USE_ESP32_CAMERA
+bool APIServerConnectionBase::send_camera_image_response(const CameraImageResponse &msg) {
+  ESP_LOGVV(TAG, "send_camera_image_response: %s", msg.dump().c_str());
+  return this->send_message_<CameraImageResponse>(msg, 44);
+}
+#endif
+#ifdef USE_ESP32_CAMERA
+#endif
+#ifdef USE_CLIMATE
+bool APIServerConnectionBase::send_list_entities_climate_response(const ListEntitiesClimateResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_climate_response: %s", msg.dump().c_str());
+  return this->send_message_<ListEntitiesClimateResponse>(msg, 46);
+}
+#endif
+#ifdef USE_CLIMATE
+bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_climate_state_response: %s", msg.dump().c_str());
+  return this->send_message_<ClimateStateResponse>(msg, 47);
+}
+#endif
+#ifdef USE_CLIMATE
+#endif
+bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
+  switch (msg_type) {
+    case 1: {
+      HelloRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str());
+      this->on_hello_request(msg);
+      break;
+    }
+    case 3: {
+      ConnectRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str());
+      this->on_connect_request(msg);
+      break;
+    }
+    case 5: {
+      DisconnectRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str());
+      this->on_disconnect_request(msg);
+      break;
+    }
+    case 6: {
+      DisconnectResponse msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str());
+      this->on_disconnect_response(msg);
+      break;
+    }
+    case 7: {
+      PingRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str());
+      this->on_ping_request(msg);
+      break;
+    }
+    case 8: {
+      PingResponse msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str());
+      this->on_ping_response(msg);
+      break;
+    }
+    case 9: {
+      DeviceInfoRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str());
+      this->on_device_info_request(msg);
+      break;
+    }
+    case 11: {
+      ListEntitiesRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str());
+      this->on_list_entities_request(msg);
+      break;
+    }
+    case 20: {
+      SubscribeStatesRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str());
+      this->on_subscribe_states_request(msg);
+      break;
+    }
+    case 28: {
+      SubscribeLogsRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_subscribe_logs_request: %s", msg.dump().c_str());
+      this->on_subscribe_logs_request(msg);
+      break;
+    }
+    case 30: {
+#ifdef USE_COVER
+      CoverCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str());
+      this->on_cover_command_request(msg);
+#endif
+      break;
+    }
+    case 31: {
+#ifdef USE_FAN
+      FanCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str());
+      this->on_fan_command_request(msg);
+#endif
+      break;
+    }
+    case 32: {
+#ifdef USE_LIGHT
+      LightCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str());
+      this->on_light_command_request(msg);
+#endif
+      break;
+    }
+    case 33: {
+#ifdef USE_SWITCH
+      SwitchCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str());
+      this->on_switch_command_request(msg);
+#endif
+      break;
+    }
+    case 34: {
+      SubscribeHomeassistantServicesRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str());
+      this->on_subscribe_homeassistant_services_request(msg);
+      break;
+    }
+    case 36: {
+      GetTimeRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str());
+      this->on_get_time_request(msg);
+      break;
+    }
+    case 37: {
+      GetTimeResponse msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_get_time_response: %s", msg.dump().c_str());
+      this->on_get_time_response(msg);
+      break;
+    }
+    case 38: {
+      SubscribeHomeAssistantStatesRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str());
+      this->on_subscribe_home_assistant_states_request(msg);
+      break;
+    }
+    case 40: {
+      HomeAssistantStateResponse msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_home_assistant_state_response: %s", msg.dump().c_str());
+      this->on_home_assistant_state_response(msg);
+      break;
+    }
+    case 42: {
+      ExecuteServiceRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_execute_service_request: %s", msg.dump().c_str());
+      this->on_execute_service_request(msg);
+      break;
+    }
+    case 45: {
+#ifdef USE_ESP32_CAMERA
+      CameraImageRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str());
+      this->on_camera_image_request(msg);
+#endif
+      break;
+    }
+    case 48: {
+#ifdef USE_CLIMATE
+      ClimateCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str());
+      this->on_climate_command_request(msg);
+#endif
+      break;
+    }
+    default:
+      return false;
+  }
+  return true;
+}
+
+void APIServerConnection::on_hello_request(const HelloRequest &msg) {
+  HelloResponse ret = this->hello(msg);
+  if (!this->send_hello_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
+  ConnectResponse ret = this->connect(msg);
+  if (!this->send_connect_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
+  DisconnectResponse ret = this->disconnect(msg);
+  if (!this->send_disconnect_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_ping_request(const PingRequest &msg) {
+  PingResponse ret = this->ping(msg);
+  if (!this->send_ping_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  DeviceInfoResponse ret = this->device_info(msg);
+  if (!this->send_device_info_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->list_entities(msg);
+}
+void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->subscribe_states(msg);
+}
+void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->subscribe_logs(msg);
+}
+void APIServerConnection::on_subscribe_homeassistant_services_request(
+    const SubscribeHomeassistantServicesRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->subscribe_homeassistant_services(msg);
+}
+void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->subscribe_home_assistant_states(msg);
+}
+void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  GetTimeResponse ret = this->get_time(msg);
+  if (!this->send_get_time_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->execute_service(msg);
+}
+#ifdef USE_COVER
+void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->cover_command(msg);
+}
+#endif
+#ifdef USE_FAN
+void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->fan_command(msg);
+}
+#endif
+#ifdef USE_LIGHT
+void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->light_command(msg);
+}
+#endif
+#ifdef USE_SWITCH
+void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->switch_command(msg);
+}
+#endif
+#ifdef USE_ESP32_CAMERA
+void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->camera_image(msg);
+}
+#endif
+#ifdef USE_CLIMATE
+void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->climate_command(msg);
+}
+#endif
+
+}  // namespace api
+}  // namespace esphome

+ 185 - 0
livingroom/src/esphome/components/api/api_pb2_service.h

@@ -0,0 +1,185 @@
+// This file was automatically generated with a tool.
+// See scripts/api_protobuf/api_protobuf.py
+#pragma once
+
+#include "api_pb2.h"
+#include "esphome/core/defines.h"
+
+namespace esphome {
+namespace api {
+
+class APIServerConnectionBase : public ProtoService {
+ public:
+  virtual void on_hello_request(const HelloRequest &value){};
+  bool send_hello_response(const HelloResponse &msg);
+  virtual void on_connect_request(const ConnectRequest &value){};
+  bool send_connect_response(const ConnectResponse &msg);
+  bool send_disconnect_request(const DisconnectRequest &msg);
+  virtual void on_disconnect_request(const DisconnectRequest &value){};
+  bool send_disconnect_response(const DisconnectResponse &msg);
+  virtual void on_disconnect_response(const DisconnectResponse &value){};
+  bool send_ping_request(const PingRequest &msg);
+  virtual void on_ping_request(const PingRequest &value){};
+  bool send_ping_response(const PingResponse &msg);
+  virtual void on_ping_response(const PingResponse &value){};
+  virtual void on_device_info_request(const DeviceInfoRequest &value){};
+  bool send_device_info_response(const DeviceInfoResponse &msg);
+  virtual void on_list_entities_request(const ListEntitiesRequest &value){};
+  bool send_list_entities_done_response(const ListEntitiesDoneResponse &msg);
+  virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){};
+#ifdef USE_BINARY_SENSOR
+  bool send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg);
+#endif
+#ifdef USE_BINARY_SENSOR
+  bool send_binary_sensor_state_response(const BinarySensorStateResponse &msg);
+#endif
+#ifdef USE_COVER
+  bool send_list_entities_cover_response(const ListEntitiesCoverResponse &msg);
+#endif
+#ifdef USE_COVER
+  bool send_cover_state_response(const CoverStateResponse &msg);
+#endif
+#ifdef USE_COVER
+  virtual void on_cover_command_request(const CoverCommandRequest &value){};
+#endif
+#ifdef USE_FAN
+  bool send_list_entities_fan_response(const ListEntitiesFanResponse &msg);
+#endif
+#ifdef USE_FAN
+  bool send_fan_state_response(const FanStateResponse &msg);
+#endif
+#ifdef USE_FAN
+  virtual void on_fan_command_request(const FanCommandRequest &value){};
+#endif
+#ifdef USE_LIGHT
+  bool send_list_entities_light_response(const ListEntitiesLightResponse &msg);
+#endif
+#ifdef USE_LIGHT
+  bool send_light_state_response(const LightStateResponse &msg);
+#endif
+#ifdef USE_LIGHT
+  virtual void on_light_command_request(const LightCommandRequest &value){};
+#endif
+#ifdef USE_SENSOR
+  bool send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg);
+#endif
+#ifdef USE_SENSOR
+  bool send_sensor_state_response(const SensorStateResponse &msg);
+#endif
+#ifdef USE_SWITCH
+  bool send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg);
+#endif
+#ifdef USE_SWITCH
+  bool send_switch_state_response(const SwitchStateResponse &msg);
+#endif
+#ifdef USE_SWITCH
+  virtual void on_switch_command_request(const SwitchCommandRequest &value){};
+#endif
+#ifdef USE_TEXT_SENSOR
+  bool send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg);
+#endif
+#ifdef USE_TEXT_SENSOR
+  bool send_text_sensor_state_response(const TextSensorStateResponse &msg);
+#endif
+  virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
+  bool send_subscribe_logs_response(const SubscribeLogsResponse &msg);
+  virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
+  bool send_homeassistant_service_response(const HomeassistantServiceResponse &msg);
+  virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
+  bool send_subscribe_home_assistant_state_response(const SubscribeHomeAssistantStateResponse &msg);
+  virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
+  bool send_get_time_request(const GetTimeRequest &msg);
+  virtual void on_get_time_request(const GetTimeRequest &value){};
+  bool send_get_time_response(const GetTimeResponse &msg);
+  virtual void on_get_time_response(const GetTimeResponse &value){};
+  bool send_list_entities_services_response(const ListEntitiesServicesResponse &msg);
+  virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
+#ifdef USE_ESP32_CAMERA
+  bool send_list_entities_camera_response(const ListEntitiesCameraResponse &msg);
+#endif
+#ifdef USE_ESP32_CAMERA
+  bool send_camera_image_response(const CameraImageResponse &msg);
+#endif
+#ifdef USE_ESP32_CAMERA
+  virtual void on_camera_image_request(const CameraImageRequest &value){};
+#endif
+#ifdef USE_CLIMATE
+  bool send_list_entities_climate_response(const ListEntitiesClimateResponse &msg);
+#endif
+#ifdef USE_CLIMATE
+  bool send_climate_state_response(const ClimateStateResponse &msg);
+#endif
+#ifdef USE_CLIMATE
+  virtual void on_climate_command_request(const ClimateCommandRequest &value){};
+#endif
+ protected:
+  bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
+};
+
+class APIServerConnection : public APIServerConnectionBase {
+ public:
+  virtual HelloResponse hello(const HelloRequest &msg) = 0;
+  virtual ConnectResponse connect(const ConnectRequest &msg) = 0;
+  virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0;
+  virtual PingResponse ping(const PingRequest &msg) = 0;
+  virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0;
+  virtual void list_entities(const ListEntitiesRequest &msg) = 0;
+  virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
+  virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
+  virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
+  virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
+  virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
+  virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
+#ifdef USE_COVER
+  virtual void cover_command(const CoverCommandRequest &msg) = 0;
+#endif
+#ifdef USE_FAN
+  virtual void fan_command(const FanCommandRequest &msg) = 0;
+#endif
+#ifdef USE_LIGHT
+  virtual void light_command(const LightCommandRequest &msg) = 0;
+#endif
+#ifdef USE_SWITCH
+  virtual void switch_command(const SwitchCommandRequest &msg) = 0;
+#endif
+#ifdef USE_ESP32_CAMERA
+  virtual void camera_image(const CameraImageRequest &msg) = 0;
+#endif
+#ifdef USE_CLIMATE
+  virtual void climate_command(const ClimateCommandRequest &msg) = 0;
+#endif
+ protected:
+  void on_hello_request(const HelloRequest &msg) override;
+  void on_connect_request(const ConnectRequest &msg) override;
+  void on_disconnect_request(const DisconnectRequest &msg) override;
+  void on_ping_request(const PingRequest &msg) override;
+  void on_device_info_request(const DeviceInfoRequest &msg) override;
+  void on_list_entities_request(const ListEntitiesRequest &msg) override;
+  void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
+  void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
+  void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
+  void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
+  void on_get_time_request(const GetTimeRequest &msg) override;
+  void on_execute_service_request(const ExecuteServiceRequest &msg) override;
+#ifdef USE_COVER
+  void on_cover_command_request(const CoverCommandRequest &msg) override;
+#endif
+#ifdef USE_FAN
+  void on_fan_command_request(const FanCommandRequest &msg) override;
+#endif
+#ifdef USE_LIGHT
+  void on_light_command_request(const LightCommandRequest &msg) override;
+#endif
+#ifdef USE_SWITCH
+  void on_switch_command_request(const SwitchCommandRequest &msg) override;
+#endif
+#ifdef USE_ESP32_CAMERA
+  void on_camera_image_request(const CameraImageRequest &msg) override;
+#endif
+#ifdef USE_CLIMATE
+  void on_climate_command_request(const ClimateCommandRequest &msg) override;
+#endif
+};
+
+}  // namespace api
+}  // namespace esphome

+ 239 - 0
livingroom/src/esphome/components/api/api_server.cpp

@@ -0,0 +1,239 @@
+#include "api_server.h"
+#include "api_connection.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/core/util.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/version.h"
+
+#ifdef USE_LOGGER
+#include "esphome/components/logger/logger.h"
+#endif
+
+#include <algorithm>
+
+namespace esphome {
+namespace api {
+
+static const char *TAG = "api";
+
+// APIServer
+void APIServer::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
+  this->setup_controller();
+  this->server_ = AsyncServer(this->port_);
+  this->server_.setNoDelay(false);
+  this->server_.begin();
+  this->server_.onClient(
+      [](void *s, AsyncClient *client) {
+        if (client == nullptr)
+          return;
+
+        // can't print here because in lwIP thread
+        // ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str());
+        auto *a_this = (APIServer *) s;
+        a_this->clients_.push_back(new APIConnection(client, a_this));
+      },
+      this);
+#ifdef USE_LOGGER
+  if (logger::global_logger != nullptr) {
+    logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
+      for (auto *c : this->clients_) {
+        if (!c->remove_)
+          c->send_log_message(level, tag, message);
+      }
+    });
+  }
+#endif
+
+  this->last_connected_ = millis();
+
+#ifdef USE_ESP32_CAMERA
+  if (esp32_camera::global_esp32_camera != nullptr) {
+    esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr<esp32_camera::CameraImage> image) {
+      for (auto *c : this->clients_)
+        if (!c->remove_)
+          c->send_camera_state(image);
+    });
+  }
+#endif
+}
+void APIServer::loop() {
+  // Partition clients into remove and active
+  auto new_end =
+      std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; });
+  // print disconnection messages
+  for (auto it = new_end; it != this->clients_.end(); ++it) {
+    ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str());
+  }
+  // only then delete the pointers, otherwise log routine
+  // would access freed memory
+  for (auto it = new_end; it != this->clients_.end(); ++it)
+    delete *it;
+  // resize vector
+  this->clients_.erase(new_end, this->clients_.end());
+
+  for (auto *client : this->clients_) {
+    client->loop();
+  }
+
+  if (this->reboot_timeout_ != 0) {
+    const uint32_t now = millis();
+    if (!this->is_connected()) {
+      if (now - this->last_connected_ > this->reboot_timeout_) {
+        ESP_LOGE(TAG, "No client connected to API. Rebooting...");
+        App.reboot();
+      }
+      this->status_set_warning();
+    } else {
+      this->last_connected_ = now;
+      this->status_clear_warning();
+    }
+  }
+}
+void APIServer::dump_config() {
+  ESP_LOGCONFIG(TAG, "API Server:");
+  ESP_LOGCONFIG(TAG, "  Address: %s:%u", network_get_address().c_str(), this->port_);
+}
+bool APIServer::uses_password() const { return !this->password_.empty(); }
+bool APIServer::check_password(const std::string &password) const {
+  // depend only on input password length
+  const char *a = this->password_.c_str();
+  uint32_t len_a = this->password_.length();
+  const char *b = password.c_str();
+  uint32_t len_b = password.length();
+
+  // disable optimization with volatile
+  volatile uint32_t length = len_b;
+  volatile const char *left = nullptr;
+  volatile const char *right = b;
+  uint8_t result = 0;
+
+  if (len_a == length) {
+    left = *((volatile const char **) &a);
+    result = 0;
+  }
+  if (len_a != length) {
+    left = b;
+    result = 1;
+  }
+
+  for (size_t i = 0; i < length; i++) {
+    result |= *left++ ^ *right++;  // NOLINT
+  }
+
+  return result == 0;
+}
+void APIServer::handle_disconnect(APIConnection *conn) {}
+#ifdef USE_BINARY_SENSOR
+void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
+  if (obj->is_internal())
+    return;
+  for (auto *c : this->clients_)
+    c->send_binary_sensor_state(obj, state);
+}
+#endif
+
+#ifdef USE_COVER
+void APIServer::on_cover_update(cover::Cover *obj) {
+  if (obj->is_internal())
+    return;
+  for (auto *c : this->clients_)
+    c->send_cover_state(obj);
+}
+#endif
+
+#ifdef USE_FAN
+void APIServer::on_fan_update(fan::FanState *obj) {
+  if (obj->is_internal())
+    return;
+  for (auto *c : this->clients_)
+    c->send_fan_state(obj);
+}
+#endif
+
+#ifdef USE_LIGHT
+void APIServer::on_light_update(light::LightState *obj) {
+  if (obj->is_internal())
+    return;
+  for (auto *c : this->clients_)
+    c->send_light_state(obj);
+}
+#endif
+
+#ifdef USE_SENSOR
+void APIServer::on_sensor_update(sensor::Sensor *obj, float state) {
+  if (obj->is_internal())
+    return;
+  for (auto *c : this->clients_)
+    c->send_sensor_state(obj, state);
+}
+#endif
+
+#ifdef USE_SWITCH
+void APIServer::on_switch_update(switch_::Switch *obj, bool state) {
+  if (obj->is_internal())
+    return;
+  for (auto *c : this->clients_)
+    c->send_switch_state(obj, state);
+}
+#endif
+
+#ifdef USE_TEXT_SENSOR
+void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) {
+  if (obj->is_internal())
+    return;
+  for (auto *c : this->clients_)
+    c->send_text_sensor_state(obj, state);
+}
+#endif
+
+#ifdef USE_CLIMATE
+void APIServer::on_climate_update(climate::Climate *obj) {
+  if (obj->is_internal())
+    return;
+  for (auto *c : this->clients_)
+    c->send_climate_state(obj);
+}
+#endif
+
+float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
+void APIServer::set_port(uint16_t port) { this->port_ = port; }
+APIServer *global_api_server = nullptr;
+
+void APIServer::set_password(const std::string &password) { this->password_ = password; }
+void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
+  for (auto *client : this->clients_) {
+    client->send_homeassistant_service_call(call);
+  }
+}
+APIServer::APIServer() { global_api_server = this; }
+void APIServer::subscribe_home_assistant_state(std::string entity_id, std::function<void(std::string)> f) {
+  this->state_subs_.push_back(HomeAssistantStateSubscription{
+      .entity_id = std::move(entity_id),
+      .callback = std::move(f),
+  });
+}
+const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
+  return this->state_subs_;
+}
+uint16_t APIServer::get_port() const { return this->port_; }
+void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
+#ifdef USE_HOMEASSISTANT_TIME
+void APIServer::request_time() {
+  for (auto *client : this->clients_) {
+    if (!client->remove_ && client->connection_state_ == APIConnection::ConnectionState::CONNECTED)
+      client->send_time_request();
+  }
+}
+#endif
+bool APIServer::is_connected() const { return !this->clients_.empty(); }
+void APIServer::on_shutdown() {
+  for (auto *c : this->clients_) {
+    c->send_disconnect_request(DisconnectRequest());
+  }
+  delay(10);
+}
+
+}  // namespace api
+}  // namespace esphome

+ 100 - 0
livingroom/src/esphome/components/api/api_server.h

@@ -0,0 +1,100 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/controller.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/log.h"
+#include "api_pb2.h"
+#include "api_pb2_service.h"
+#include "util.h"
+#include "list_entities.h"
+#include "subscribe_state.h"
+#include "homeassistant_service.h"
+#include "user_services.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+#include <AsyncTCP.h>
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+#include <ESPAsyncTCP.h>
+#endif
+
+namespace esphome {
+namespace api {
+
+class APIServer : public Component, public Controller {
+ public:
+  APIServer();
+  void setup() override;
+  uint16_t get_port() const;
+  float get_setup_priority() const override;
+  void loop() override;
+  void dump_config() override;
+  void on_shutdown() override;
+  bool check_password(const std::string &password) const;
+  bool uses_password() const;
+  void set_port(uint16_t port);
+  void set_password(const std::string &password);
+  void set_reboot_timeout(uint32_t reboot_timeout);
+  void handle_disconnect(APIConnection *conn);
+#ifdef USE_BINARY_SENSOR
+  void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
+#endif
+#ifdef USE_COVER
+  void on_cover_update(cover::Cover *obj) override;
+#endif
+#ifdef USE_FAN
+  void on_fan_update(fan::FanState *obj) override;
+#endif
+#ifdef USE_LIGHT
+  void on_light_update(light::LightState *obj) override;
+#endif
+#ifdef USE_SENSOR
+  void on_sensor_update(sensor::Sensor *obj, float state) override;
+#endif
+#ifdef USE_SWITCH
+  void on_switch_update(switch_::Switch *obj, bool state) override;
+#endif
+#ifdef USE_TEXT_SENSOR
+  void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) override;
+#endif
+#ifdef USE_CLIMATE
+  void on_climate_update(climate::Climate *obj) override;
+#endif
+  void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
+  void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
+#ifdef USE_HOMEASSISTANT_TIME
+  void request_time();
+#endif
+
+  bool is_connected() const;
+
+  struct HomeAssistantStateSubscription {
+    std::string entity_id;
+    std::function<void(std::string)> callback;
+  };
+
+  void subscribe_home_assistant_state(std::string entity_id, std::function<void(std::string)> f);
+  const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
+  const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
+
+ protected:
+  AsyncServer server_{0};
+  uint16_t port_{6053};
+  uint32_t reboot_timeout_{300000};
+  uint32_t last_connected_{0};
+  std::vector<APIConnection *> clients_;
+  std::string password_;
+  std::vector<HomeAssistantStateSubscription> state_subs_;
+  std::vector<UserServiceDescriptor *> user_services_;
+};
+
+extern APIServer *global_api_server;
+
+template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
+ public:
+  bool check(Ts... x) override { return global_api_server->is_connected(); }
+};
+
+}  // namespace api
+}  // namespace esphome

+ 214 - 0
livingroom/src/esphome/components/api/custom_api_device.h

@@ -0,0 +1,214 @@
+#pragma once
+
+#include <map>
+#include "user_services.h"
+#include "api_server.h"
+
+namespace esphome {
+namespace api {
+
+template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
+ public:
+  CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
+                         void (T::*callback)(Ts...))
+      : UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
+
+ protected:
+  void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); }  // NOLINT
+
+  T *obj_;
+  void (T::*callback_)(Ts...);
+};
+
+class CustomAPIDevice {
+ public:
+  /// Return if a client (such as Home Assistant) is connected to the native API.
+  bool is_connected() const { return global_api_server->is_connected(); }
+
+  /** Register a custom native API service that will show up in Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * void setup() override {
+   *   register_service(&CustomNativeAPI::on_start_washer_cycle, "start_washer_cycle",
+   *                    {"cycle_length"});
+   * }
+   *
+   * void on_start_washer_cycle(int cycle_length) {
+   *   // Start washer cycle.
+   * }
+   * ```
+   *
+   * @tparam T The class type creating the service, automatically deduced from the function pointer.
+   * @tparam Ts The argument types for the service, automatically deduced from the function arguments.
+   * @param callback The member function to call when the service is triggered.
+   * @param name The name of the service to register.
+   * @param arg_names The name of the arguments for the service, must match the arguments of the function.
+   */
+  template<typename T, typename... Ts>
+  void register_service(void (T::*callback)(Ts...), const std::string &name,
+                        const std::array<std::string, sizeof...(Ts)> &arg_names) {
+    auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);
+    global_api_server->register_user_service(service);
+  }
+
+  /** Register a custom native API service that will show up in Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * void setup() override {
+   *   register_service(&CustomNativeAPI::on_hello_world, "hello_world");
+   * }
+   *
+   * void on_hello_world() {
+   *   // Hello World service called.
+   * }
+   * ```
+   *
+   * @tparam T The class type creating the service, automatically deduced from the function pointer.
+   * @param callback The member function to call when the service is triggered.
+   * @param name The name of the arguments for the service, must match the arguments of the function.
+   */
+  template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
+    auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);
+    global_api_server->register_user_service(service);
+  }
+
+  /** Subscribe to the state of an entity from Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * void setup() override {
+   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
+   * }
+   *
+   * void on_state_changed(std::string state) {
+   *   // State of sensor.weather_forecast is `state`
+   * }
+   * ```
+   *
+   * @tparam T The class type creating the service, automatically deduced from the function pointer.
+   * @param callback The member function to call when the entity state changes.
+   * @param entity_id The entity_id to track.
+   */
+  template<typename T>
+  void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id) {
+    auto f = std::bind(callback, (T *) this, std::placeholders::_1);
+    global_api_server->subscribe_home_assistant_state(entity_id, f);
+  }
+
+  /** Subscribe to the state of an entity from Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * void setup() override {
+   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
+   * }
+   *
+   * void on_state_changed(std::string entity_id, std::string state) {
+   *   // State of `entity_id` is `state`
+   * }
+   * ```
+   *
+   * @tparam T The class type creating the service, automatically deduced from the function pointer.
+   * @param callback The member function to call when the entity state changes.
+   * @param entity_id The entity_id to track.
+   */
+  template<typename T>
+  void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id) {
+    auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
+    global_api_server->subscribe_home_assistant_state(entity_id, f);
+  }
+
+  /** Call a Home Assistant service from ESPHome.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * call_homeassistant_service("homeassistant.restart");
+   * ```
+   *
+   * @param service_name The service to call.
+   */
+  void call_homeassistant_service(const std::string &service_name) {
+    HomeassistantServiceResponse resp;
+    resp.service = service_name;
+    global_api_server->send_homeassistant_service_call(resp);
+  }
+
+  /** Call a Home Assistant service from ESPHome.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * call_homeassistant_service("light.turn_on", {
+   *   {"entity_id", "light.my_light"},
+   *   {"brightness", "127"},
+   * });
+   * ```
+   *
+   * @param service_name The service to call.
+   * @param data The data for the service call, mapping from string to string.
+   */
+  void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
+    HomeassistantServiceResponse resp;
+    resp.service = service_name;
+    for (auto &it : data) {
+      HomeassistantServiceMap kv;
+      kv.key = it.first;
+      kv.value = it.second;
+      resp.data.push_back(kv);
+    }
+    global_api_server->send_homeassistant_service_call(resp);
+  }
+
+  /** Fire an ESPHome event in Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * fire_homeassistant_event("esphome.something_happened");
+   * ```
+   *
+   * @param event_name The event to fire.
+   */
+  void fire_homeassistant_event(const std::string &event_name) {
+    HomeassistantServiceResponse resp;
+    resp.service = event_name;
+    resp.is_event = true;
+    global_api_server->send_homeassistant_service_call(resp);
+  }
+
+  /** Fire an ESPHome event in Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * fire_homeassistant_event("esphome.something_happened", {
+   *   {"my_value", "500"},
+   * });
+   * ```
+   *
+   * @param event_name The event to fire.
+   * @param data The data for the event, mapping from string to string.
+   */
+  void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
+    HomeassistantServiceResponse resp;
+    resp.service = service_name;
+    resp.is_event = true;
+    for (auto &it : data) {
+      HomeassistantServiceMap kv;
+      kv.key = it.first;
+      kv.value = it.second;
+      resp.data.push_back(kv);
+    }
+    global_api_server->send_homeassistant_service_call(resp);
+  }
+};
+
+}  // namespace api
+}  // namespace esphome

+ 67 - 0
livingroom/src/esphome/components/api/homeassistant_service.h

@@ -0,0 +1,67 @@
+#pragma once
+
+#include "esphome/core/helpers.h"
+#include "esphome/core/automation.h"
+#include "api_pb2.h"
+#include "api_server.h"
+
+namespace esphome {
+namespace api {
+
+template<typename... Ts> class TemplatableKeyValuePair {
+ public:
+  template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
+  std::string key;
+  TemplatableStringValue<Ts...> value;
+};
+
+template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
+ public:
+  explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
+
+  TEMPLATABLE_STRING_VALUE(service);
+  template<typename T> void add_data(std::string key, T value) {
+    this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
+  }
+  template<typename T> void add_data_template(std::string key, T value) {
+    this->data_template_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
+  }
+  template<typename T> void add_variable(std::string key, T value) {
+    this->variables_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
+  }
+
+  void play(Ts... x) override {
+    HomeassistantServiceResponse resp;
+    resp.service = this->service_.value(x...);
+    resp.is_event = this->is_event_;
+    for (auto &it : this->data_) {
+      HomeassistantServiceMap kv;
+      kv.key = it.key;
+      kv.value = it.value.value(x...);
+      resp.data.push_back(kv);
+    }
+    for (auto &it : this->data_template_) {
+      HomeassistantServiceMap kv;
+      kv.key = it.key;
+      kv.value = it.value.value(x...);
+      resp.data_template.push_back(kv);
+    }
+    for (auto &it : this->variables_) {
+      HomeassistantServiceMap kv;
+      kv.key = it.key;
+      kv.value = it.value.value(x...);
+      resp.variables.push_back(kv);
+    }
+    this->parent_->send_homeassistant_service_call(resp);
+  }
+
+ protected:
+  APIServer *parent_;
+  bool is_event_;
+  std::vector<TemplatableKeyValuePair<Ts...>> data_;
+  std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
+  std::vector<TemplatableKeyValuePair<Ts...>> variables_;
+};
+
+}  // namespace api
+}  // namespace esphome

+ 55 - 0
livingroom/src/esphome/components/api/list_entities.cpp

@@ -0,0 +1,55 @@
+#include "list_entities.h"
+#include "esphome/core/util.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "api_connection.h"
+
+namespace esphome {
+namespace api {
+
+#ifdef USE_BINARY_SENSOR
+bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
+  return this->client_->send_binary_sensor_info(binary_sensor);
+}
+#endif
+#ifdef USE_COVER
+bool ListEntitiesIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_info(cover); }
+#endif
+#ifdef USE_FAN
+bool ListEntitiesIterator::on_fan(fan::FanState *fan) { return this->client_->send_fan_info(fan); }
+#endif
+#ifdef USE_LIGHT
+bool ListEntitiesIterator::on_light(light::LightState *light) { return this->client_->send_light_info(light); }
+#endif
+#ifdef USE_SENSOR
+bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_info(sensor); }
+#endif
+#ifdef USE_SWITCH
+bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); }
+#endif
+#ifdef USE_TEXT_SENSOR
+bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
+  return this->client_->send_text_sensor_info(text_sensor);
+}
+#endif
+
+bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); }
+ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client)
+    : ComponentIterator(server), client_(client) {}
+bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
+  auto resp = service->encode_list_service_response();
+  return this->client_->send_list_entities_services_response(resp);
+}
+
+#ifdef USE_ESP32_CAMERA
+bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
+  return this->client_->send_camera_info(camera);
+}
+#endif
+
+#ifdef USE_CLIMATE
+bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); }
+#endif
+
+}  // namespace api
+}  // namespace esphome

+ 52 - 0
livingroom/src/esphome/components/api/list_entities.h

@@ -0,0 +1,52 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/defines.h"
+#include "util.h"
+
+namespace esphome {
+namespace api {
+
+class APIConnection;
+
+class ListEntitiesIterator : public ComponentIterator {
+ public:
+  ListEntitiesIterator(APIServer *server, APIConnection *client);
+#ifdef USE_BINARY_SENSOR
+  bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
+#endif
+#ifdef USE_COVER
+  bool on_cover(cover::Cover *cover) override;
+#endif
+#ifdef USE_FAN
+  bool on_fan(fan::FanState *fan) override;
+#endif
+#ifdef USE_LIGHT
+  bool on_light(light::LightState *light) override;
+#endif
+#ifdef USE_SENSOR
+  bool on_sensor(sensor::Sensor *sensor) override;
+#endif
+#ifdef USE_SWITCH
+  bool on_switch(switch_::Switch *a_switch) override;
+#endif
+#ifdef USE_TEXT_SENSOR
+  bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
+#endif
+  bool on_service(UserServiceDescriptor *service) override;
+#ifdef USE_ESP32_CAMERA
+  bool on_camera(esp32_camera::ESP32Camera *camera) override;
+#endif
+#ifdef USE_CLIMATE
+  bool on_climate(climate::Climate *climate) override;
+#endif
+  bool on_end() override;
+
+ protected:
+  APIConnection *client_;
+};
+
+}  // namespace api
+}  // namespace esphome
+
+#include "api_server.h"

+ 90 - 0
livingroom/src/esphome/components/api/proto.cpp

@@ -0,0 +1,90 @@
+#include "proto.h"
+#include "util.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+static const char *TAG = "api.proto";
+
+void ProtoMessage::decode(const uint8_t *buffer, size_t length) {
+  uint32_t i = 0;
+  bool error = false;
+  while (i < length) {
+    uint32_t consumed;
+    auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
+    if (!res.has_value()) {
+      ESP_LOGV(TAG, "Invalid field start at %u", i);
+      break;
+    }
+
+    uint32_t field_type = (res->as_uint32()) & 0b111;
+    uint32_t field_id = (res->as_uint32()) >> 3;
+    i += consumed;
+
+    switch (field_type) {
+      case 0: {  // VarInt
+        res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
+        if (!res.has_value()) {
+          ESP_LOGV(TAG, "Invalid VarInt at %u", i);
+          error = true;
+          break;
+        }
+        if (!this->decode_varint(field_id, *res)) {
+          ESP_LOGV(TAG, "Cannot decode VarInt field %u with value %u!", field_id, res->as_uint32());
+        }
+        i += consumed;
+        break;
+      }
+      case 2: {  // Length-delimited
+        res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
+        if (!res.has_value()) {
+          ESP_LOGV(TAG, "Invalid Length Delimited at %u", i);
+          error = true;
+          break;
+        }
+        uint32_t field_length = res->as_uint32();
+        i += consumed;
+        if (field_length > length - i) {
+          ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %u", i);
+          error = true;
+          break;
+        }
+        if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) {
+          ESP_LOGV(TAG, "Cannot decode Length Delimited field %u!", field_id);
+        }
+        i += field_length;
+        break;
+      }
+      case 5: {  // 32-bit
+        if (length - i < 4) {
+          ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %u", i);
+          error = true;
+          break;
+        }
+        uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]);
+        if (!this->decode_32bit(field_id, Proto32Bit(val))) {
+          ESP_LOGV(TAG, "Cannot decode 32-bit field %u with value %u!", field_id, val);
+        }
+        i += 4;
+        break;
+      }
+      default:
+        ESP_LOGV(TAG, "Invalid field type at %u", i);
+        error = true;
+        break;
+    }
+    if (error) {
+      break;
+    }
+  }
+}
+
+std::string ProtoMessage::dump() const {
+  std::string out;
+  this->dump_to(out);
+  return out;
+}
+
+}  // namespace api
+}  // namespace esphome

+ 278 - 0
livingroom/src/esphome/components/api/proto.h

@@ -0,0 +1,278 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace api {
+
+/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
+class ProtoVarInt {
+ public:
+  ProtoVarInt() : value_(0) {}
+  explicit ProtoVarInt(uint64_t value) : value_(value) {}
+
+  static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
+    if (consumed != nullptr)
+      *consumed = 0;
+
+    if (len == 0)
+      return {};
+
+    uint64_t result = 0;
+    uint8_t bitpos = 0;
+
+    for (uint32_t i = 0; i < len; i++) {
+      uint8_t val = buffer[i];
+      result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
+      bitpos += 7;
+      if ((val & 0x80) == 0) {
+        if (consumed != nullptr)
+          *consumed = i + 1;
+        return ProtoVarInt(result);
+      }
+    }
+
+    return {};
+  }
+
+  uint32_t as_uint32() const { return this->value_; }
+  uint64_t as_uint64() const { return this->value_; }
+  bool as_bool() const { return this->value_; }
+  template<typename T> T as_enum() const { return static_cast<T>(this->as_uint32()); }
+  int32_t as_int32() const {
+    // Not ZigZag encoded
+    return static_cast<int32_t>(this->as_int64());
+  }
+  int64_t as_int64() const {
+    // Not ZigZag encoded
+    return static_cast<int64_t>(this->value_);
+  }
+  int32_t as_sint32() const {
+    // with ZigZag encoding
+    if (this->value_ & 1)
+      return static_cast<int32_t>(~(this->value_ >> 1));
+    else
+      return static_cast<int32_t>(this->value_ >> 1);
+  }
+  int64_t as_sint64() const {
+    // with ZigZag encoding
+    if (this->value_ & 1)
+      return static_cast<int64_t>(~(this->value_ >> 1));
+    else
+      return static_cast<int64_t>(this->value_ >> 1);
+  }
+  void encode(std::vector<uint8_t> &out) {
+    uint32_t val = this->value_;
+    if (val <= 0x7F) {
+      out.push_back(val);
+      return;
+    }
+    while (val) {
+      uint8_t temp = val & 0x7F;
+      val >>= 7;
+      if (val) {
+        out.push_back(temp | 0x80);
+      } else {
+        out.push_back(temp);
+      }
+    }
+  }
+
+ protected:
+  uint64_t value_;
+};
+
+class ProtoLengthDelimited {
+ public:
+  explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
+  std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
+  template<class C> C as_message() const {
+    auto msg = C();
+    msg.decode(this->value_, this->length_);
+    return msg;
+  }
+
+ protected:
+  const uint8_t *const value_;
+  const size_t length_;
+};
+
+class Proto32Bit {
+ public:
+  explicit Proto32Bit(uint32_t value) : value_(value) {}
+  uint32_t as_fixed32() const { return this->value_; }
+  int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); }
+  float as_float() const {
+    union {
+      uint32_t raw;
+      float value;
+    } s{};
+    s.raw = this->value_;
+    return s.value;
+  }
+
+ protected:
+  const uint32_t value_;
+};
+
+class Proto64Bit {
+ public:
+  explicit Proto64Bit(uint64_t value) : value_(value) {}
+  uint64_t as_fixed64() const { return this->value_; }
+  int64_t as_sfixed64() const { return static_cast<int64_t>(this->value_); }
+  double as_double() const {
+    union {
+      uint64_t raw;
+      double value;
+    } s{};
+    s.raw = this->value_;
+    return s.value;
+  }
+
+ protected:
+  const uint64_t value_;
+};
+
+class ProtoWriteBuffer {
+ public:
+  ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
+  void write(uint8_t value) { this->buffer_->push_back(value); }
+  void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
+  void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
+  void encode_field_raw(uint32_t field_id, uint32_t type) {
+    uint32_t val = (field_id << 3) | (type & 0b111);
+    this->encode_varint_raw(val);
+  }
+  void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
+    if (len == 0 && !force)
+      return;
+
+    this->encode_field_raw(field_id, 2);
+    this->encode_varint_raw(len);
+    auto *data = reinterpret_cast<const uint8_t *>(string);
+    for (size_t i = 0; i < len; i++)
+      this->write(data[i]);
+  }
+  void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
+    this->encode_string(field_id, value.data(), value.size());
+  }
+  void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
+    this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
+  }
+  void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) {
+    if (value == 0 && !force)
+      return;
+    this->encode_field_raw(field_id, 0);
+    this->encode_varint_raw(value);
+  }
+  void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
+    if (value == 0 && !force)
+      return;
+    this->encode_field_raw(field_id, 0);
+    this->encode_varint_raw(ProtoVarInt(value));
+  }
+  void encode_bool(uint32_t field_id, bool value, bool force = false) {
+    if (!value && !force)
+      return;
+    this->encode_field_raw(field_id, 0);
+    this->write(0x01);
+  }
+  void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
+    if (value == 0 && !force)
+      return;
+
+    this->encode_field_raw(field_id, 5);
+    this->write((value >> 0) & 0xFF);
+    this->write((value >> 8) & 0xFF);
+    this->write((value >> 16) & 0xFF);
+    this->write((value >> 24) & 0xFF);
+  }
+  template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) {
+    this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
+  }
+  void encode_float(uint32_t field_id, float value, bool force = false) {
+    if (value == 0.0f && !force)
+      return;
+
+    union {
+      float value;
+      uint32_t raw;
+    } val{};
+    val.value = value;
+    this->encode_fixed32(field_id, val.raw);
+  }
+  void encode_int32(uint32_t field_id, int32_t value, bool force = false) {
+    if (value < 0) {
+      // negative int32 is always 10 byte long
+      this->encode_int64(field_id, value, force);
+      return;
+    }
+    this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
+  }
+  void encode_int64(uint32_t field_id, int64_t value, bool force = false) {
+    this->encode_uint64(field_id, static_cast<uint64_t>(value), force);
+  }
+  void encode_sint32(uint32_t field_id, int32_t value, bool force = false) {
+    uint32_t uvalue;
+    if (value < 0)
+      uvalue = ~(value << 1);
+    else
+      uvalue = value << 1;
+    this->encode_uint32(field_id, uvalue, force);
+  }
+  template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
+    this->encode_field_raw(field_id, 2);
+    size_t begin = this->buffer_->size();
+
+    value.encode(*this);
+
+    const uint32_t nested_length = this->buffer_->size() - begin;
+    // add size varint
+    std::vector<uint8_t> var;
+    ProtoVarInt(nested_length).encode(var);
+    this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end());
+  }
+  std::vector<uint8_t> *get_buffer() const { return buffer_; }
+
+ protected:
+  std::vector<uint8_t> *buffer_;
+};
+
+class ProtoMessage {
+ public:
+  virtual void encode(ProtoWriteBuffer buffer) const = 0;
+  void decode(const uint8_t *buffer, size_t length);
+  std::string dump() const;
+  virtual void dump_to(std::string &out) const = 0;
+
+ protected:
+  virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
+  virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
+  virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
+  virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; }
+};
+
+template<typename T> const char *proto_enum_to_string(T value);
+
+class ProtoService {
+ public:
+ protected:
+  virtual bool is_authenticated() = 0;
+  virtual bool is_connection_setup() = 0;
+  virtual void on_fatal_error() = 0;
+  virtual void on_unauthenticated_access() = 0;
+  virtual void on_no_setup_connection() = 0;
+  virtual ProtoWriteBuffer create_buffer() = 0;
+  virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0;
+  virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
+
+  template<class C> bool send_message_(const C &msg, uint32_t message_type) {
+    auto buffer = this->create_buffer();
+    msg.encode(buffer);
+    return this->send_buffer(buffer, message_type);
+  }
+};
+
+}  // namespace api
+}  // namespace esphome

+ 44 - 0
livingroom/src/esphome/components/api/subscribe_state.cpp

@@ -0,0 +1,44 @@
+#include "subscribe_state.h"
+#include "api_connection.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+#ifdef USE_BINARY_SENSOR
+bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
+  return this->client_->send_binary_sensor_state(binary_sensor, binary_sensor->state);
+}
+#endif
+#ifdef USE_COVER
+bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); }
+#endif
+#ifdef USE_FAN
+bool InitialStateIterator::on_fan(fan::FanState *fan) { return this->client_->send_fan_state(fan); }
+#endif
+#ifdef USE_LIGHT
+bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); }
+#endif
+#ifdef USE_SENSOR
+bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) {
+  return this->client_->send_sensor_state(sensor, sensor->state);
+}
+#endif
+#ifdef USE_SWITCH
+bool InitialStateIterator::on_switch(switch_::Switch *a_switch) {
+  return this->client_->send_switch_state(a_switch, a_switch->state);
+}
+#endif
+#ifdef USE_TEXT_SENSOR
+bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
+  return this->client_->send_text_sensor_state(text_sensor, text_sensor->state);
+}
+#endif
+#ifdef USE_CLIMATE
+bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); }
+#endif
+InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
+    : ComponentIterator(server), client_(client) {}
+
+}  // namespace api
+}  // namespace esphome

+ 47 - 0
livingroom/src/esphome/components/api/subscribe_state.h

@@ -0,0 +1,47 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/controller.h"
+#include "esphome/core/defines.h"
+#include "util.h"
+
+namespace esphome {
+namespace api {
+
+class APIConnection;
+
+class InitialStateIterator : public ComponentIterator {
+ public:
+  InitialStateIterator(APIServer *server, APIConnection *client);
+#ifdef USE_BINARY_SENSOR
+  bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
+#endif
+#ifdef USE_COVER
+  bool on_cover(cover::Cover *cover) override;
+#endif
+#ifdef USE_FAN
+  bool on_fan(fan::FanState *fan) override;
+#endif
+#ifdef USE_LIGHT
+  bool on_light(light::LightState *light) override;
+#endif
+#ifdef USE_SENSOR
+  bool on_sensor(sensor::Sensor *sensor) override;
+#endif
+#ifdef USE_SWITCH
+  bool on_switch(switch_::Switch *a_switch) override;
+#endif
+#ifdef USE_TEXT_SENSOR
+  bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
+#endif
+#ifdef USE_CLIMATE
+  bool on_climate(climate::Climate *climate) override;
+#endif
+ protected:
+  APIConnection *client_;
+};
+
+}  // namespace api
+}  // namespace esphome
+
+#include "api_server.h"

+ 42 - 0
livingroom/src/esphome/components/api/user_services.cpp

@@ -0,0 +1,42 @@
+#include "user_services.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
+template<> int get_execute_arg_value<int>(const ExecuteServiceArgument &arg) {
+  if (arg.legacy_int != 0)
+    return arg.legacy_int;
+  return arg.int_;
+}
+template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
+template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
+template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
+  return arg.bool_array;
+}
+template<> std::vector<int> get_execute_arg_value<std::vector<int>>(const ExecuteServiceArgument &arg) {
+  return arg.int_array;
+}
+template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
+  return arg.float_array;
+}
+template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
+  return arg.string_array;
+}
+
+template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
+template<> enums::ServiceArgType to_service_arg_type<int>() { return enums::SERVICE_ARG_TYPE_INT; }
+template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
+template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
+template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
+template<> enums::ServiceArgType to_service_arg_type<std::vector<int>>() { return enums::SERVICE_ARG_TYPE_INT_ARRAY; }
+template<> enums::ServiceArgType to_service_arg_type<std::vector<float>>() {
+  return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY;
+}
+template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>() {
+  return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
+}
+
+}  // namespace api
+}  // namespace esphome

+ 72 - 0
livingroom/src/esphome/components/api/user_services.h

@@ -0,0 +1,72 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "api_pb2.h"
+
+namespace esphome {
+namespace api {
+
+class UserServiceDescriptor {
+ public:
+  virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
+
+  virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
+};
+
+template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
+
+template<typename T> enums::ServiceArgType to_service_arg_type();
+
+template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
+ public:
+  UserServiceBase(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
+      : name_(name), arg_names_(arg_names) {
+    this->key_ = fnv1_hash(this->name_);
+  }
+
+  ListEntitiesServicesResponse encode_list_service_response() override {
+    ListEntitiesServicesResponse msg;
+    msg.name = this->name_;
+    msg.key = this->key_;
+    std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
+    for (int i = 0; i < sizeof...(Ts); i++) {
+      ListEntitiesServicesArgument arg;
+      arg.type = arg_types[i];
+      arg.name = this->arg_names_[i];
+      msg.args.push_back(arg);
+    }
+    return msg;
+  }
+
+  bool execute_service(const ExecuteServiceRequest &req) override {
+    if (req.key != this->key_)
+      return false;
+    if (req.args.size() != this->arg_names_.size())
+      return false;
+    this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
+    return true;
+  }
+
+ protected:
+  virtual void execute(Ts... x) = 0;
+  template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...>) {
+    this->execute((get_execute_arg_value<Ts>(args[S]))...);
+  }
+
+  std::string name_;
+  uint32_t key_{0};
+  std::array<std::string, sizeof...(Ts)> arg_names_;
+};
+
+template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
+ public:
+  UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
+      : UserServiceBase<Ts...>(name, arg_names) {}
+
+ protected:
+  void execute(Ts... x) override { this->trigger(x...); }  // NOLINT
+};
+
+}  // namespace api
+}  // namespace esphome

+ 193 - 0
livingroom/src/esphome/components/api/util.cpp

@@ -0,0 +1,193 @@
+#include "util.h"
+#include "api_server.h"
+#include "user_services.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+namespace api {
+
+ComponentIterator::ComponentIterator(APIServer *server) : server_(server) {}
+void ComponentIterator::begin() {
+  this->state_ = IteratorState::BEGIN;
+  this->at_ = 0;
+}
+void ComponentIterator::advance() {
+  bool advance_platform = false;
+  bool success = true;
+  switch (this->state_) {
+    case IteratorState::NONE:
+      // not started
+      return;
+    case IteratorState::BEGIN:
+      if (this->on_begin()) {
+        advance_platform = true;
+      } else {
+        return;
+      }
+      break;
+#ifdef USE_BINARY_SENSOR
+    case IteratorState::BINARY_SENSOR:
+      if (this->at_ >= App.get_binary_sensors().size()) {
+        advance_platform = true;
+      } else {
+        auto *binary_sensor = App.get_binary_sensors()[this->at_];
+        if (binary_sensor->is_internal()) {
+          success = true;
+          break;
+        } else {
+          success = this->on_binary_sensor(binary_sensor);
+        }
+      }
+      break;
+#endif
+#ifdef USE_COVER
+    case IteratorState::COVER:
+      if (this->at_ >= App.get_covers().size()) {
+        advance_platform = true;
+      } else {
+        auto *cover = App.get_covers()[this->at_];
+        if (cover->is_internal()) {
+          success = true;
+          break;
+        } else {
+          success = this->on_cover(cover);
+        }
+      }
+      break;
+#endif
+#ifdef USE_FAN
+    case IteratorState::FAN:
+      if (this->at_ >= App.get_fans().size()) {
+        advance_platform = true;
+      } else {
+        auto *fan = App.get_fans()[this->at_];
+        if (fan->is_internal()) {
+          success = true;
+          break;
+        } else {
+          success = this->on_fan(fan);
+        }
+      }
+      break;
+#endif
+#ifdef USE_LIGHT
+    case IteratorState::LIGHT:
+      if (this->at_ >= App.get_lights().size()) {
+        advance_platform = true;
+      } else {
+        auto *light = App.get_lights()[this->at_];
+        if (light->is_internal()) {
+          success = true;
+          break;
+        } else {
+          success = this->on_light(light);
+        }
+      }
+      break;
+#endif
+#ifdef USE_SENSOR
+    case IteratorState::SENSOR:
+      if (this->at_ >= App.get_sensors().size()) {
+        advance_platform = true;
+      } else {
+        auto *sensor = App.get_sensors()[this->at_];
+        if (sensor->is_internal()) {
+          success = true;
+          break;
+        } else {
+          success = this->on_sensor(sensor);
+        }
+      }
+      break;
+#endif
+#ifdef USE_SWITCH
+    case IteratorState::SWITCH:
+      if (this->at_ >= App.get_switches().size()) {
+        advance_platform = true;
+      } else {
+        auto *a_switch = App.get_switches()[this->at_];
+        if (a_switch->is_internal()) {
+          success = true;
+          break;
+        } else {
+          success = this->on_switch(a_switch);
+        }
+      }
+      break;
+#endif
+#ifdef USE_TEXT_SENSOR
+    case IteratorState::TEXT_SENSOR:
+      if (this->at_ >= App.get_text_sensors().size()) {
+        advance_platform = true;
+      } else {
+        auto *text_sensor = App.get_text_sensors()[this->at_];
+        if (text_sensor->is_internal()) {
+          success = true;
+          break;
+        } else {
+          success = this->on_text_sensor(text_sensor);
+        }
+      }
+      break;
+#endif
+    case IteratorState ::SERVICE:
+      if (this->at_ >= this->server_->get_user_services().size()) {
+        advance_platform = true;
+      } else {
+        auto *service = this->server_->get_user_services()[this->at_];
+        success = this->on_service(service);
+      }
+      break;
+#ifdef USE_ESP32_CAMERA
+    case IteratorState::CAMERA:
+      if (esp32_camera::global_esp32_camera == nullptr) {
+        advance_platform = true;
+      } else {
+        if (esp32_camera::global_esp32_camera->is_internal()) {
+          advance_platform = success = true;
+          break;
+        } else {
+          advance_platform = success = this->on_camera(esp32_camera::global_esp32_camera);
+        }
+      }
+      break;
+#endif
+#ifdef USE_CLIMATE
+    case IteratorState::CLIMATE:
+      if (this->at_ >= App.get_climates().size()) {
+        advance_platform = true;
+      } else {
+        auto *climate = App.get_climates()[this->at_];
+        if (climate->is_internal()) {
+          success = true;
+          break;
+        } else {
+          success = this->on_climate(climate);
+        }
+      }
+      break;
+#endif
+    case IteratorState::MAX:
+      if (this->on_end()) {
+        this->state_ = IteratorState::NONE;
+      }
+      return;
+  }
+
+  if (advance_platform) {
+    this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1);
+    this->at_ = 0;
+  } else if (success) {
+    this->at_++;
+  }
+}
+bool ComponentIterator::on_end() { return true; }
+bool ComponentIterator::on_begin() { return true; }
+bool ComponentIterator::on_service(UserServiceDescriptor *service) { return true; }
+#ifdef USE_ESP32_CAMERA
+bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; }
+#endif
+
+}  // namespace api
+}  // namespace esphome

+ 93 - 0
livingroom/src/esphome/components/api/util.h

@@ -0,0 +1,93 @@
+#pragma once
+
+#include "esphome/core/helpers.h"
+#include "esphome/core/component.h"
+#include "esphome/core/controller.h"
+#ifdef USE_ESP32_CAMERA
+#include "esphome/components/esp32_camera/esp32_camera.h"
+#endif
+
+namespace esphome {
+namespace api {
+
+class APIServer;
+class UserServiceDescriptor;
+
+class ComponentIterator {
+ public:
+  ComponentIterator(APIServer *server);
+
+  void begin();
+  void advance();
+  virtual bool on_begin();
+#ifdef USE_BINARY_SENSOR
+  virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0;
+#endif
+#ifdef USE_COVER
+  virtual bool on_cover(cover::Cover *cover) = 0;
+#endif
+#ifdef USE_FAN
+  virtual bool on_fan(fan::FanState *fan) = 0;
+#endif
+#ifdef USE_LIGHT
+  virtual bool on_light(light::LightState *light) = 0;
+#endif
+#ifdef USE_SENSOR
+  virtual bool on_sensor(sensor::Sensor *sensor) = 0;
+#endif
+#ifdef USE_SWITCH
+  virtual bool on_switch(switch_::Switch *a_switch) = 0;
+#endif
+#ifdef USE_TEXT_SENSOR
+  virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0;
+#endif
+  virtual bool on_service(UserServiceDescriptor *service);
+#ifdef USE_ESP32_CAMERA
+  virtual bool on_camera(esp32_camera::ESP32Camera *camera);
+#endif
+#ifdef USE_CLIMATE
+  virtual bool on_climate(climate::Climate *climate) = 0;
+#endif
+  virtual bool on_end();
+
+ protected:
+  enum class IteratorState {
+    NONE = 0,
+    BEGIN,
+#ifdef USE_BINARY_SENSOR
+    BINARY_SENSOR,
+#endif
+#ifdef USE_COVER
+    COVER,
+#endif
+#ifdef USE_FAN
+    FAN,
+#endif
+#ifdef USE_LIGHT
+    LIGHT,
+#endif
+#ifdef USE_SENSOR
+    SENSOR,
+#endif
+#ifdef USE_SWITCH
+    SWITCH,
+#endif
+#ifdef USE_TEXT_SENSOR
+    TEXT_SENSOR,
+#endif
+    SERVICE,
+#ifdef USE_ESP32_CAMERA
+    CAMERA,
+#endif
+#ifdef USE_CLIMATE
+    CLIMATE,
+#endif
+    MAX,
+  } state_{IteratorState::NONE};
+  size_t at_{0};
+
+  APIServer *server_;
+};
+
+}  // namespace api
+}  // namespace esphome

+ 174 - 0
livingroom/src/esphome/components/captive_portal/captive_portal.cpp

@@ -0,0 +1,174 @@
+#include "captive_portal.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/components/wifi/wifi_component.h"
+
+namespace esphome {
+namespace captive_portal {
+
+static const char *TAG = "captive_portal";
+
+void CaptivePortal::handle_index(AsyncWebServerRequest *request) {
+  AsyncResponseStream *stream = request->beginResponseStream("text/html");
+  stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" "
+                  "content=\"width=device-width,initial-scale=1,user-scalable=no\"/><title>"));
+  stream->print(App.get_name().c_str());
+  stream->print(F("</title><link rel=\"stylesheet\" href=\"/stylesheet.css\">"));
+  stream->print(F("<script>function c(l){document.getElementById('ssid').value=l.innerText||l.textContent; "
+                  "document.getElementById('psk').focus();}</script>"));
+  stream->print(F("</head>"));
+  stream->print(F("<body><div class=\"main\"><h1>WiFi Networks</h1>"));
+
+  if (request->hasArg("save")) {
+    stream->print(F("<div class=\"info\">The ESP will now try to connect to the network...<br/>Please give it some "
+                    "time to connect.<br/>Note: Copy the changed network to your YAML file - the next OTA update will "
+                    "overwrite these settings.</div>"));
+  }
+
+  for (auto &scan : wifi::global_wifi_component->get_scan_result()) {
+    if (scan.get_is_hidden())
+      continue;
+
+    stream->print(F("<div class=\"network\" onclick=\"c(this)\"><a href=\"#\" class=\"network-left\">"));
+
+    if (scan.get_rssi() >= -50) {
+      stream->print(F("<img src=\"/wifi-strength-4.svg\">"));
+    } else if (scan.get_rssi() >= -65) {
+      stream->print(F("<img src=\"/wifi-strength-3.svg\">"));
+    } else if (scan.get_rssi() >= -85) {
+      stream->print(F("<img src=\"/wifi-strength-2.svg\">"));
+    } else {
+      stream->print(F("<img src=\"/wifi-strength-1.svg\">"));
+    }
+
+    stream->print(F("<span class=\"network-ssid\">"));
+    stream->print(scan.get_ssid().c_str());
+    stream->print(F("</span></a>"));
+    if (scan.get_with_auth()) {
+      stream->print(F("<img src=\"/lock.svg\">"));
+    }
+    stream->print(F("</div>"));
+  }
+
+  stream->print(F("<h3>WiFi Settings</h3><form method=\"GET\" action=\"/wifisave\"><input id=\"ssid\" name=\"ssid\" "
+                  "length=32 placeholder=\"SSID\"><br/><input id=\"psk\" name=\"psk\" length=64 type=\"password\" "
+                  "placeholder=\"Password\"><br/><br/><button type=\"submit\">Save</button></form><br><hr><br>"));
+  stream->print(F("<h1>OTA Update</h1><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
+                  "type=\"file\" name=\"update\"><button type=\"submit\">Update</button></form>"));
+  stream->print(F("</div></body></html>"));
+  request->send(stream);
+}
+void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
+  std::string ssid = request->arg("ssid").c_str();
+  std::string psk = request->arg("psk").c_str();
+  ESP_LOGI(TAG, "Captive Portal Requested WiFi Settings Change:");
+  ESP_LOGI(TAG, "  SSID='%s'", ssid.c_str());
+  ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str());
+  this->override_sta_(ssid, psk);
+  request->redirect("/?save=true");
+}
+void CaptivePortal::override_sta_(const std::string &ssid, const std::string &password) {
+  CaptivePortalSettings save{};
+  strcpy(save.ssid, ssid.c_str());
+  strcpy(save.password, password.c_str());
+  this->pref_.save(&save);
+
+  wifi::WiFiAP sta{};
+  sta.set_ssid(ssid);
+  sta.set_password(password);
+  wifi::global_wifi_component->set_sta(sta);
+}
+
+void CaptivePortal::setup() {
+  // Hash with compilation time
+  // This ensures the AP override is not applied for OTA
+  uint32_t hash = fnv1_hash(App.get_compilation_time());
+  this->pref_ = global_preferences.make_preference<CaptivePortalSettings>(hash, true);
+
+  CaptivePortalSettings save{};
+  if (this->pref_.load(&save)) {
+    this->override_sta_(save.ssid, save.password);
+  }
+}
+void CaptivePortal::start() {
+  this->base_->init();
+  if (!this->initialized_) {
+    this->base_->add_handler(this);
+    this->base_->add_ota_handler();
+  }
+
+  this->dns_server_ = new DNSServer();
+  this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
+  IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
+  this->dns_server_->start(53, "*", ip);
+
+  this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
+    bool not_found = false;
+    if (!this->active_) {
+      not_found = true;
+    } else if (req->host() == wifi::global_wifi_component->wifi_soft_ap_ip().toString()) {
+      not_found = true;
+    }
+
+    if (not_found) {
+      req->send(404, "text/html", "File not found");
+      return;
+    }
+
+    auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().toString();
+    req->redirect(url);
+  });
+
+  this->initialized_ = true;
+  this->active_ = true;
+}
+
+const char STYLESHEET_CSS[] PROGMEM =
+    R"(*{box-sizing:inherit}div,input{padding:5px;font-size:1em}input{width:95%}body{text-align:center;font-family:sans-serif}button{border:0;border-radius:.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;padding:0}.main{text-align:left;display:inline-block;min-width:260px}.network{display:flex;justify-content:space-between;align-items:center}.network-left{display:flex;align-items:center}.network-ssid{margin-bottom:-7px;margin-left:10px}.info{border:1px solid;margin:10px 0;padding:15px 10px;color:#4f8a10;background-color:#dff2bf})";
+const char LOCK_SVG[] PROGMEM =
+    R"(<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg>)";
+
+void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
+  if (req->url() == "/") {
+    this->handle_index(req);
+    return;
+  } else if (req->url() == "/wifisave") {
+    this->handle_wifisave(req);
+    return;
+  } else if (req->url() == "/stylesheet.css") {
+    req->send_P(200, "text/css", STYLESHEET_CSS);
+    return;
+  } else if (req->url() == "/lock.svg") {
+    req->send_P(200, "image/svg+xml", LOCK_SVG);
+    return;
+  }
+
+  AsyncResponseStream *stream = req->beginResponseStream("image/svg+xml");
+  stream->print(F("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\"><path d=\"M12 3A18.9 18.9 0 0 "
+                  "0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 "));
+  if (req->url() == "/wifi-strength-4.svg") {
+    stream->print(F("3z"));
+  } else {
+    if (req->url() == "/wifi-strength-1.svg") {
+      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.4"));
+    } else if (req->url() == "/wifi-strength-2.svg") {
+      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.4"));
+    } else if (req->url() == "/wifi-strength-3.svg") {
+      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2."));
+    }
+    stream->print(F("4A16.94 16.94 0 0 1 12 5z"));
+  }
+  stream->print(F("\"/></svg>"));
+  req->send(stream);
+}
+CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; }
+float CaptivePortal::get_setup_priority() const {
+  // Before WiFi
+  return setup_priority::WIFI + 1.0f;
+}
+void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); }
+
+CaptivePortal *global_captive_portal = nullptr;
+
+}  // namespace captive_portal
+}  // namespace esphome

+ 82 - 0
livingroom/src/esphome/components/captive_portal/captive_portal.h

@@ -0,0 +1,82 @@
+#pragma once
+
+#include <DNSServer.h>
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/preferences.h"
+#include "esphome/components/web_server_base/web_server_base.h"
+
+namespace esphome {
+
+namespace captive_portal {
+
+struct CaptivePortalSettings {
+  char ssid[33];
+  char password[65];
+} PACKED;  // NOLINT
+
+class CaptivePortal : public AsyncWebHandler, public Component {
+ public:
+  CaptivePortal(web_server_base::WebServerBase *base);
+  void setup() override;
+  void dump_config() override;
+  void loop() override {
+    if (this->dns_server_ != nullptr)
+      this->dns_server_->processNextRequest();
+  }
+  float get_setup_priority() const override;
+  void start();
+  bool is_active() const { return this->active_; }
+  void end() {
+    this->active_ = false;
+    this->base_->deinit();
+    this->dns_server_->stop();
+    delete this->dns_server_;
+  }
+
+  bool canHandle(AsyncWebServerRequest *request) override {
+    if (!this->active_)
+      return false;
+
+    if (request->method() == HTTP_GET) {
+      if (request->url() == "/")
+        return true;
+      if (request->url() == "/stylesheet.css")
+        return true;
+      if (request->url() == "/wifi-strength-1.svg")
+        return true;
+      if (request->url() == "/wifi-strength-2.svg")
+        return true;
+      if (request->url() == "/wifi-strength-3.svg")
+        return true;
+      if (request->url() == "/wifi-strength-4.svg")
+        return true;
+      if (request->url() == "/lock.svg")
+        return true;
+      if (request->url() == "/wifisave")
+        return true;
+    }
+
+    return false;
+  }
+
+  void handle_index(AsyncWebServerRequest *request);
+
+  void handle_wifisave(AsyncWebServerRequest *request);
+
+  void handleRequest(AsyncWebServerRequest *req) override;
+
+ protected:
+  void override_sta_(const std::string &ssid, const std::string &password);
+
+  web_server_base::WebServerBase *base_;
+  bool initialized_{false};
+  bool active_{false};
+  ESPPreferenceObject pref_;
+  DNSServer *dns_server_{nullptr};
+};
+
+extern CaptivePortal *global_captive_portal;
+
+}  // namespace captive_portal
+}  // namespace esphome

+ 22 - 0
livingroom/src/esphome/components/homeassistant/time/homeassistant_time.cpp

@@ -0,0 +1,22 @@
+#include "homeassistant_time.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace homeassistant {
+
+static const char *TAG = "homeassistant.time";
+
+void HomeassistantTime::dump_config() {
+  ESP_LOGCONFIG(TAG, "Home Assistant Time:");
+  ESP_LOGCONFIG(TAG, "  Timezone: '%s'", this->timezone_.c_str());
+}
+
+float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; }
+
+void HomeassistantTime::setup() { global_homeassistant_time = this; }
+
+void HomeassistantTime::update() { api::global_api_server->request_time(); }
+
+HomeassistantTime *global_homeassistant_time = nullptr;
+}  // namespace homeassistant
+}  // namespace esphome

+ 22 - 0
livingroom/src/esphome/components/homeassistant/time/homeassistant_time.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/time/real_time_clock.h"
+#include "esphome/components/api/api_server.h"
+
+namespace esphome {
+namespace homeassistant {
+
+class HomeassistantTime : public time::RealTimeClock {
+ public:
+  void setup() override;
+  void update() override;
+  void dump_config() override;
+  void set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); }
+  float get_setup_priority() const override;
+};
+
+extern HomeassistantTime *global_homeassistant_time;
+
+}  // namespace homeassistant
+}  // namespace esphome

+ 129 - 0
livingroom/src/esphome/components/json/json_util.cpp

@@ -0,0 +1,129 @@
+#include "json_util.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace json {
+
+static const char *TAG = "json";
+
+static char *global_json_build_buffer = nullptr;
+static size_t global_json_build_buffer_size = 0;
+
+void reserve_global_json_build_buffer(size_t required_size) {
+  if (global_json_build_buffer_size == 0 || global_json_build_buffer_size < required_size) {
+    delete[] global_json_build_buffer;
+    global_json_build_buffer_size = std::max(required_size, global_json_build_buffer_size * 2);
+
+    size_t remainder = global_json_build_buffer_size % 16U;
+    if (remainder != 0)
+      global_json_build_buffer_size += 16 - remainder;
+
+    global_json_build_buffer = new char[global_json_build_buffer_size];
+  }
+}
+
+const char *build_json(const json_build_t &f, size_t *length) {
+  global_json_buffer.clear();
+  JsonObject &root = global_json_buffer.createObject();
+
+  f(root);
+
+  // The Json buffer size gives us a good estimate for the required size.
+  // Usually, it's a bit larger than the actual required string size
+  //             | JSON Buffer Size | String Size |
+  // Discovery   | 388              | 351         |
+  // Discovery   | 372              | 356         |
+  // Discovery   | 336              | 311         |
+  // Discovery   | 408              | 393         |
+  reserve_global_json_build_buffer(global_json_buffer.size());
+  size_t bytes_written = root.printTo(global_json_build_buffer, global_json_build_buffer_size);
+
+  if (bytes_written >= global_json_build_buffer_size - 1) {
+    reserve_global_json_build_buffer(root.measureLength() + 1);
+    bytes_written = root.printTo(global_json_build_buffer, global_json_build_buffer_size);
+  }
+
+  *length = bytes_written;
+  return global_json_build_buffer;
+}
+void parse_json(const std::string &data, const json_parse_t &f) {
+  global_json_buffer.clear();
+  JsonObject &root = global_json_buffer.parseObject(data);
+
+  if (!root.success()) {
+    ESP_LOGW(TAG, "Parsing JSON failed.");
+    return;
+  }
+
+  f(root);
+}
+std::string build_json(const json_build_t &f) {
+  size_t len;
+  const char *c_str = build_json(f, &len);
+  return std::string(c_str, len);
+}
+
+VectorJsonBuffer::String::String(VectorJsonBuffer *parent) : parent_(parent), start_(parent->size_) {}
+void VectorJsonBuffer::String::append(char c) const {
+  char *last = static_cast<char *>(this->parent_->do_alloc(1));
+  *last = c;
+}
+const char *VectorJsonBuffer::String::c_str() const {
+  this->append('\0');
+  return &this->parent_->buffer_[this->start_];
+}
+void VectorJsonBuffer::clear() {
+  for (char *block : this->free_blocks_)
+    free(block);  // NOLINT
+
+  this->size_ = 0;
+  this->free_blocks_.clear();
+}
+VectorJsonBuffer::String VectorJsonBuffer::startString() { return {this}; }  // NOLINT
+void *VectorJsonBuffer::alloc(size_t bytes) {
+  // Make sure memory addresses are aligned
+  uint32_t new_size = round_size_up(this->size_);
+  this->resize(new_size);
+  return this->do_alloc(bytes);
+}
+void *VectorJsonBuffer::do_alloc(size_t bytes) {  // NOLINT
+  const uint32_t begin = this->size_;
+  this->resize(begin + bytes);
+  return &this->buffer_[begin];
+}
+void VectorJsonBuffer::resize(size_t size) {  // NOLINT
+  if (size <= this->size_) {
+    this->size_ = size;
+    return;
+  }
+
+  this->reserve(size);
+  this->size_ = size;
+}
+void VectorJsonBuffer::reserve(size_t size) {  // NOLINT
+  if (size <= this->capacity_)
+    return;
+
+  uint32_t target_capacity = this->capacity_;
+  if (this->capacity_ == 0) {
+    // lazily initialize with a reasonable size
+    target_capacity = JSON_OBJECT_SIZE(16);
+  }
+  while (target_capacity < size)
+    target_capacity *= 2;
+
+  char *old_buffer = this->buffer_;
+  this->buffer_ = new char[target_capacity];
+  if (old_buffer != nullptr && this->capacity_ != 0) {
+    this->free_blocks_.push_back(old_buffer);
+    memcpy(this->buffer_, old_buffer, this->capacity_);
+  }
+  this->capacity_ = target_capacity;
+}
+
+size_t VectorJsonBuffer::size() const { return this->size_; }
+
+VectorJsonBuffer global_json_buffer;
+
+}  // namespace json
+}  // namespace esphome

+ 62 - 0
livingroom/src/esphome/components/json/json_util.h

@@ -0,0 +1,62 @@
+#pragma once
+
+#include "esphome/core/helpers.h"
+#include <ArduinoJson.h>
+
+namespace esphome {
+namespace json {
+
+/// Callback function typedef for parsing JsonObjects.
+using json_parse_t = std::function<void(JsonObject &)>;
+
+/// Callback function typedef for building JsonObjects.
+using json_build_t = std::function<void(JsonObject &)>;
+
+/// Build a JSON string with the provided json build function.
+const char *build_json(const json_build_t &f, size_t *length);
+
+std::string build_json(const json_build_t &f);
+
+/// Parse a JSON string and run the provided json parse function if it's valid.
+void parse_json(const std::string &data, const json_parse_t &f);
+
+class VectorJsonBuffer : public ArduinoJson::Internals::JsonBufferBase<VectorJsonBuffer> {
+ public:
+  class String {
+   public:
+    String(VectorJsonBuffer *parent);
+
+    void append(char c) const;
+
+    const char *c_str() const;
+
+   protected:
+    VectorJsonBuffer *parent_;
+    uint32_t start_;
+  };
+
+  void *alloc(size_t bytes) override;
+
+  size_t size() const;
+
+  void clear();
+
+  String startString();  // NOLINT
+
+ protected:
+  void *do_alloc(size_t bytes);  // NOLINT
+
+  void resize(size_t size);  // NOLINT
+
+  void reserve(size_t size);  // NOLINT
+
+  char *buffer_{nullptr};
+  size_t size_{0};
+  size_t capacity_{0};
+  std::vector<char *> free_blocks_;
+};
+
+extern VectorJsonBuffer global_json_buffer;
+
+}  // namespace json
+}  // namespace esphome

+ 198 - 0
livingroom/src/esphome/components/logger/logger.cpp

@@ -0,0 +1,198 @@
+#include "logger.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+#include <esp_log.h>
+#endif
+#include <HardwareSerial.h>
+
+namespace esphome {
+namespace logger {
+
+static const char *TAG = "logger";
+
+static const char *LOG_LEVEL_COLORS[] = {
+    "",                                            // NONE
+    ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED),       // ERROR
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW),   // WARNING
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN),    // INFO
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA),  // CONFIG
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN),     // DEBUG
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY),     // VERBOSE
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE),    // VERY_VERBOSE
+};
+static const char *LOG_LEVEL_LETTERS[] = {
+    "",    // NONE
+    "E",   // ERROR
+    "W",   // WARNING
+    "I",   // INFO
+    "C",   // CONFIG
+    "D",   // DEBUG
+    "V",   // VERBOSE
+    "VV",  // VERY_VERBOSE
+};
+
+void Logger::write_header_(int level, const char *tag, int line) {
+  if (level < 0)
+    level = 0;
+  if (level > 7)
+    level = 7;
+
+  const char *color = LOG_LEVEL_COLORS[level];
+  const char *letter = LOG_LEVEL_LETTERS[level];
+  this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, line);
+}
+
+void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) {  // NOLINT
+  if (level > this->level_for(tag))
+    return;
+
+  this->reset_buffer_();
+  this->write_header_(level, tag, line);
+  this->vprintf_to_buffer_(format, args);
+  this->write_footer_();
+  this->log_message_(level, tag);
+}
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
+                          va_list args) {  // NOLINT
+  if (level > this->level_for(tag))
+    return;
+
+  this->reset_buffer_();
+  // copy format string
+  const char *format_pgm_p = (PGM_P) format;
+  size_t len = 0;
+  char ch = '.';
+  while (!this->is_buffer_full_() && ch != '\0') {
+    this->tx_buffer_[this->tx_buffer_at_++] = ch = pgm_read_byte(format_pgm_p++);
+  }
+  // Buffer full form copying format
+  if (this->is_buffer_full_())
+    return;
+
+  // length of format string, includes null terminator
+  uint32_t offset = this->tx_buffer_at_;
+
+  // now apply vsnprintf
+  this->write_header_(level, tag, line);
+  this->vprintf_to_buffer_(this->tx_buffer_, args);
+  this->write_footer_();
+  this->log_message_(level, tag, offset);
+}
+#endif
+
+int HOT Logger::level_for(const char *tag) {
+  // Uses std::vector<> for low memory footprint, though the vector
+  // could be sorted to minimize lookup times. This feature isn't used that
+  // much anyway so it doesn't matter too much.
+  for (auto &it : this->log_levels_) {
+    if (it.tag == tag) {
+      return it.level;
+    }
+  }
+  return ESPHOME_LOG_LEVEL;
+}
+void HOT Logger::log_message_(int level, const char *tag, int offset) {
+  // remove trailing newline
+  if (this->tx_buffer_[this->tx_buffer_at_ - 1] == '\n') {
+    this->tx_buffer_at_--;
+  }
+  // make sure null terminator is present
+  this->set_null_terminator_();
+
+  const char *msg = this->tx_buffer_ + offset;
+  if (this->baud_rate_ > 0)
+    this->hw_serial_->println(msg);
+#ifdef ARDUINO_ARCH_ESP32
+  // Suppress network-logging if memory constrained, but still log to serial
+  // ports. In some configurations (eg BLE enabled) there may be some transient
+  // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping
+  // here usually allows the stack to recover instead.
+  // See issue #1234 for analysis.
+  if (xPortGetFreeHeapSize() > 2048)
+    this->log_callback_.call(level, tag, msg);
+#else
+  this->log_callback_.call(level, tag, msg);
+#endif
+}
+
+Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart)
+    : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size), uart_(uart) {
+  // add 1 to buffer size for null terminator
+  this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];
+}
+
+void Logger::pre_setup() {
+  if (this->baud_rate_ > 0) {
+    switch (this->uart_) {
+      case UART_SELECTION_UART0:
+#ifdef ARDUINO_ARCH_ESP8266
+      case UART_SELECTION_UART0_SWAP:
+#endif
+        this->hw_serial_ = &Serial;
+        break;
+      case UART_SELECTION_UART1:
+        this->hw_serial_ = &Serial1;
+        break;
+#ifdef ARDUINO_ARCH_ESP32
+      case UART_SELECTION_UART2:
+        this->hw_serial_ = &Serial2;
+        break;
+#endif
+    }
+
+    this->hw_serial_->begin(this->baud_rate_);
+#ifdef ARDUINO_ARCH_ESP8266
+    if (this->uart_ == UART_SELECTION_UART0_SWAP) {
+      this->hw_serial_->swap();
+    }
+    this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
+#endif
+  }
+#ifdef ARDUINO_ARCH_ESP8266
+  else {
+    uart_set_debug(UART_NO);
+  }
+#endif
+
+  global_logger = this;
+#ifdef ARDUINO_ARCH_ESP32
+  esp_log_set_vprintf(esp_idf_log_vprintf_);
+  if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
+    esp_log_level_set("*", ESP_LOG_VERBOSE);
+  }
+#endif
+
+  ESP_LOGI(TAG, "Log initialized");
+}
+void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
+void Logger::set_log_level(const std::string &tag, int log_level) {
+  this->log_levels_.push_back(LogLevelOverride{tag, log_level});
+}
+UARTSelection Logger::get_uart() const { return this->uart_; }
+void Logger::add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback) {
+  this->log_callback_.add(std::move(callback));
+}
+float Logger::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
+const char *LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
+#ifdef ARDUINO_ARCH_ESP32
+const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART2"};
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
+#endif
+void Logger::dump_config() {
+  ESP_LOGCONFIG(TAG, "Logger:");
+  ESP_LOGCONFIG(TAG, "  Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
+  ESP_LOGCONFIG(TAG, "  Log Baud Rate: %u", this->baud_rate_);
+  ESP_LOGCONFIG(TAG, "  Hardware UART: %s", UART_SELECTIONS[this->uart_]);
+  for (auto &it : this->log_levels_) {
+    ESP_LOGCONFIG(TAG, "  Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]);
+  }
+}
+void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); }
+
+Logger *global_logger = nullptr;
+
+}  // namespace logger
+}  // namespace esphome

+ 137 - 0
livingroom/src/esphome/components/logger/logger.h

@@ -0,0 +1,137 @@
+#pragma once
+
+#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/defines.h"
+
+namespace esphome {
+
+namespace logger {
+
+/** Enum for logging UART selection
+ *
+ * Advanced configuration (pin selection, etc) is not supported.
+ */
+enum UARTSelection {
+  UART_SELECTION_UART0 = 0,
+  UART_SELECTION_UART1,
+#ifdef ARDUINO_ARCH_ESP32
+  UART_SELECTION_UART2
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+      UART_SELECTION_UART0_SWAP
+#endif
+};
+
+class Logger : public Component {
+ public:
+  explicit Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart);
+
+  /// Manually set the baud rate for serial, set to 0 to disable.
+  void set_baud_rate(uint32_t baud_rate);
+  uint32_t get_baud_rate() const { return baud_rate_; }
+  HardwareSerial *get_hw_serial() const { return hw_serial_; }
+
+  /// Get the UART used by the logger.
+  UARTSelection get_uart() const;
+
+  /// Set the log level of the specified tag.
+  void set_log_level(const std::string &tag, int log_level);
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  /// Set up this component.
+  void pre_setup();
+  void dump_config() override;
+
+  int level_for(const char *tag);
+
+  /// Register a callback that will be called for every log message sent
+  void add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback);
+
+  float get_setup_priority() const override;
+
+  void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args);  // NOLINT
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+  void log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args);  // NOLINT
+#endif
+
+ protected:
+  void write_header_(int level, const char *tag, int line);
+  void write_footer_();
+  void log_message_(int level, const char *tag, int offset = 0);
+
+  inline bool is_buffer_full_() const { return this->tx_buffer_at_ >= this->tx_buffer_size_; }
+  inline int buffer_remaining_capacity_() const { return this->tx_buffer_size_ - this->tx_buffer_at_; }
+  inline void reset_buffer_() { this->tx_buffer_at_ = 0; }
+  inline void set_null_terminator_() {
+    // does not increment buffer_at
+    this->tx_buffer_[this->tx_buffer_at_] = '\0';
+  }
+  inline void write_to_buffer_(char value) {
+    if (!this->is_buffer_full_())
+      this->tx_buffer_[this->tx_buffer_at_++] = value;
+  }
+  inline void write_to_buffer_(const char *value, int length) {
+    for (int i = 0; i < length && !this->is_buffer_full_(); i++) {
+      this->tx_buffer_[this->tx_buffer_at_++] = value[i];
+    }
+  }
+  inline void vprintf_to_buffer_(const char *format, va_list args) {
+    if (this->is_buffer_full_())
+      return;
+    int remaining = this->buffer_remaining_capacity_();
+    int ret = vsnprintf(this->tx_buffer_ + this->tx_buffer_at_, remaining, format, args);
+    if (ret < 0) {
+      // Encoding error, do not increment buffer_at
+      return;
+    }
+    if (ret >= remaining) {
+      // output was too long, truncated
+      ret = remaining;
+    }
+    this->tx_buffer_at_ += ret;
+  }
+  inline void printf_to_buffer_(const char *format, ...) {
+    va_list arg;
+    va_start(arg, format);
+    this->vprintf_to_buffer_(format, arg);
+    va_end(arg);
+  }
+
+  uint32_t baud_rate_;
+  char *tx_buffer_{nullptr};
+  int tx_buffer_at_{0};
+  int tx_buffer_size_{0};
+  UARTSelection uart_{UART_SELECTION_UART0};
+  HardwareSerial *hw_serial_{nullptr};
+  struct LogLevelOverride {
+    std::string tag;
+    int level;
+  };
+  std::vector<LogLevelOverride> log_levels_;
+  CallbackManager<void(int, const char *, const char *)> log_callback_{};
+};
+
+extern Logger *global_logger;
+
+class LoggerMessageTrigger : public Trigger<int, const char *, const char *> {
+ public:
+  explicit LoggerMessageTrigger(Logger *parent, int level) {
+    this->level_ = level;
+    parent->add_on_log_callback([this](int level, const char *tag, const char *message) {
+      if (level <= this->level_) {
+        this->trigger(level, tag, message);
+      }
+    });
+  }
+
+ protected:
+  int level_;
+};
+
+}  // namespace logger
+
+}  // namespace esphome

+ 404 - 0
livingroom/src/esphome/components/ota/ota_component.cpp

@@ -0,0 +1,404 @@
+#include "ota_component.h"
+
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/application.h"
+#include "esphome/core/util.h"
+
+#include <cstdio>
+#include <MD5Builder.h>
+#ifdef ARDUINO_ARCH_ESP32
+#include <Update.h>
+#endif
+#include <StreamString.h>
+
+namespace esphome {
+namespace ota {
+
+static const char *TAG = "ota";
+
+uint8_t OTA_VERSION_1_0 = 1;
+
+void OTAComponent::setup() {
+  this->server_ = new WiFiServer(this->port_);
+  this->server_->begin();
+
+  this->dump_config();
+}
+void OTAComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "Over-The-Air Updates:");
+  ESP_LOGCONFIG(TAG, "  Address: %s:%u", network_get_address().c_str(), this->port_);
+  if (!this->password_.empty()) {
+    ESP_LOGCONFIG(TAG, "  Using Password.");
+  }
+  if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1) {
+    ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %d restarts",
+             this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_);
+  }
+}
+
+void OTAComponent::loop() {
+  this->handle_();
+
+  if (this->has_safe_mode_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) {
+    this->has_safe_mode_ = false;
+    // successful boot, reset counter
+    ESP_LOGI(TAG, "Boot seems successful, resetting boot loop counter.");
+    this->clean_rtc();
+  }
+}
+
+void OTAComponent::handle_() {
+  OTAResponseTypes error_code = OTA_RESPONSE_ERROR_UNKNOWN;
+  bool update_started = false;
+  uint32_t total = 0;
+  uint32_t last_progress = 0;
+  uint8_t buf[1024];
+  char *sbuf = reinterpret_cast<char *>(buf);
+  uint32_t ota_size;
+  uint8_t ota_features;
+  (void) ota_features;
+
+  if (!this->client_.connected()) {
+    this->client_ = this->server_->available();
+
+    if (!this->client_.connected())
+      return;
+  }
+
+  // enable nodelay for outgoing data
+  this->client_.setNoDelay(true);
+
+  ESP_LOGD(TAG, "Starting OTA Update from %s...", this->client_.remoteIP().toString().c_str());
+  this->status_set_warning();
+
+  if (!this->wait_receive_(buf, 5)) {
+    ESP_LOGW(TAG, "Reading magic bytes failed!");
+    goto error;
+  }
+  // 0x6C, 0x26, 0xF7, 0x5C, 0x45
+  if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) {
+    ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3],
+             buf[4]);
+    error_code = OTA_RESPONSE_ERROR_MAGIC;
+    goto error;
+  }
+
+  // Send OK and version - 2 bytes
+  this->client_.write(OTA_RESPONSE_OK);
+  this->client_.write(OTA_VERSION_1_0);
+
+  // Read features - 1 byte
+  if (!this->wait_receive_(buf, 1)) {
+    ESP_LOGW(TAG, "Reading features failed!");
+    goto error;
+  }
+  ota_features = buf[0];  // NOLINT
+  ESP_LOGV(TAG, "OTA features is 0x%02X", ota_features);
+
+  // Acknowledge header - 1 byte
+  this->client_.write(OTA_RESPONSE_HEADER_OK);
+
+  if (!this->password_.empty()) {
+    this->client_.write(OTA_RESPONSE_REQUEST_AUTH);
+    MD5Builder md5_builder{};
+    md5_builder.begin();
+    sprintf(sbuf, "%08X", random_uint32());
+    md5_builder.add(sbuf);
+    md5_builder.calculate();
+    md5_builder.getChars(sbuf);
+    ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf);
+
+    // Send nonce, 32 bytes hex MD5
+    if (this->client_.write(reinterpret_cast<uint8_t *>(sbuf), 32) != 32) {
+      ESP_LOGW(TAG, "Auth: Writing nonce failed!");
+      goto error;
+    }
+
+    // prepare challenge
+    md5_builder.begin();
+    md5_builder.add(this->password_.c_str());
+    // add nonce
+    md5_builder.add(sbuf);
+
+    // Receive cnonce, 32 bytes hex MD5
+    if (!this->wait_receive_(buf, 32)) {
+      ESP_LOGW(TAG, "Auth: Reading cnonce failed!");
+      goto error;
+    }
+    sbuf[32] = '\0';
+    ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf);
+    // add cnonce
+    md5_builder.add(sbuf);
+
+    // calculate result
+    md5_builder.calculate();
+    md5_builder.getChars(sbuf);
+    ESP_LOGV(TAG, "Auth: Result is %s", sbuf);
+
+    // Receive result, 32 bytes hex MD5
+    if (!this->wait_receive_(buf + 64, 32)) {
+      ESP_LOGW(TAG, "Auth: Reading response failed!");
+      goto error;
+    }
+    sbuf[64 + 32] = '\0';
+    ESP_LOGV(TAG, "Auth: Response is %s", sbuf + 64);
+
+    bool matches = true;
+    for (uint8_t i = 0; i < 32; i++)
+      matches = matches && buf[i] == buf[64 + i];
+
+    if (!matches) {
+      ESP_LOGW(TAG, "Auth failed! Passwords do not match!");
+      error_code = OTA_RESPONSE_ERROR_AUTH_INVALID;
+      goto error;
+    }
+  }
+
+  // Acknowledge auth OK - 1 byte
+  this->client_.write(OTA_RESPONSE_AUTH_OK);
+
+  // Read size, 4 bytes MSB first
+  if (!this->wait_receive_(buf, 4)) {
+    ESP_LOGW(TAG, "Reading size failed!");
+    goto error;
+  }
+  ota_size = 0;
+  for (uint8_t i = 0; i < 4; i++) {
+    ota_size <<= 8;
+    ota_size |= buf[i];
+  }
+  ESP_LOGV(TAG, "OTA size is %u bytes", ota_size);
+
+#ifdef ARDUINO_ARCH_ESP8266
+  global_preferences.prevent_write(true);
+#endif
+
+  if (!Update.begin(ota_size, U_FLASH)) {
+    StreamString ss;
+    Update.printError(ss);
+#ifdef ARDUINO_ARCH_ESP8266
+    if (ss.indexOf("Invalid bootstrapping") != -1) {
+      error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING;
+      goto error;
+    }
+    if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) {
+      error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
+      goto error;
+    }
+    if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) {
+      error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
+      goto error;
+    }
+    if (ss.indexOf("Not Enough Space") != -1) {
+      error_code = OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE;
+      goto error;
+    }
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+    if (ss.indexOf("Bad Size Given") != -1) {
+      error_code = OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
+      goto error;
+    }
+#endif
+    ESP_LOGW(TAG, "Preparing OTA partition failed! '%s'", ss.c_str());
+    error_code = OTA_RESPONSE_ERROR_UPDATE_PREPARE;
+    goto error;
+  }
+  update_started = true;
+
+  // Acknowledge prepare OK - 1 byte
+  this->client_.write(OTA_RESPONSE_UPDATE_PREPARE_OK);
+
+  // Read binary MD5, 32 bytes
+  if (!this->wait_receive_(buf, 32)) {
+    ESP_LOGW(TAG, "Reading binary MD5 checksum failed!");
+    goto error;
+  }
+  sbuf[32] = '\0';
+  ESP_LOGV(TAG, "Update: Binary MD5 is %s", sbuf);
+  Update.setMD5(sbuf);
+
+  // Acknowledge MD5 OK - 1 byte
+  this->client_.write(OTA_RESPONSE_BIN_MD5_OK);
+
+  while (!Update.isFinished()) {
+    size_t available = this->wait_receive_(buf, 0);
+    if (!available) {
+      goto error;
+    }
+
+    uint32_t written = Update.write(buf, available);
+    if (written != available) {
+      ESP_LOGW(TAG, "Error writing binary data to flash: %u != %u!", written, available);  // NOLINT
+      error_code = OTA_RESPONSE_ERROR_WRITING_FLASH;
+      goto error;
+    }
+    total += written;
+
+    uint32_t now = millis();
+    if (now - last_progress > 1000) {
+      last_progress = now;
+      float percentage = (total * 100.0f) / ota_size;
+      ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
+      // slow down OTA update to avoid getting killed by task watchdog (task_wdt)
+      delay(10);
+    }
+  }
+
+  // Acknowledge receive OK - 1 byte
+  this->client_.write(OTA_RESPONSE_RECEIVE_OK);
+
+  if (!Update.end()) {
+    error_code = OTA_RESPONSE_ERROR_UPDATE_END;
+    goto error;
+  }
+
+  // Acknowledge Update end OK - 1 byte
+  this->client_.write(OTA_RESPONSE_UPDATE_END_OK);
+
+  // Read ACK
+  if (!this->wait_receive_(buf, 1, false) || buf[0] != OTA_RESPONSE_OK) {
+    ESP_LOGW(TAG, "Reading back acknowledgement failed!");
+    // do not go to error, this is not fatal
+  }
+
+  this->client_.flush();
+  this->client_.stop();
+  delay(10);
+  ESP_LOGI(TAG, "OTA update finished!");
+  this->status_clear_warning();
+  delay(100);  // NOLINT
+  App.safe_reboot();
+
+error:
+  if (update_started) {
+    StreamString ss;
+    Update.printError(ss);
+    ESP_LOGW(TAG, "Update end failed! Error: %s", ss.c_str());
+  }
+  if (this->client_.connected()) {
+    this->client_.write(static_cast<uint8_t>(error_code));
+    this->client_.flush();
+  }
+  this->client_.stop();
+
+#ifdef ARDUINO_ARCH_ESP32
+  if (update_started) {
+    Update.abort();
+  }
+#endif
+
+#ifdef ARDUINO_ARCH_ESP8266
+  if (update_started) {
+    Update.end();
+  }
+#endif
+
+  this->status_momentary_error("onerror", 5000);
+
+#ifdef ARDUINO_ARCH_ESP8266
+  global_preferences.prevent_write(false);
+#endif
+}
+
+size_t OTAComponent::wait_receive_(uint8_t *buf, size_t bytes, bool check_disconnected) {
+  size_t available = 0;
+  uint32_t start = millis();
+  do {
+    App.feed_wdt();
+    if (check_disconnected && !this->client_.connected()) {
+      ESP_LOGW(TAG, "Error client disconnected while receiving data!");
+      return 0;
+    }
+    int availi = this->client_.available();
+    if (availi < 0) {
+      ESP_LOGW(TAG, "Error reading data!");
+      return 0;
+    }
+    uint32_t now = millis();
+    if (availi == 0 && now - start > 10000) {
+      ESP_LOGW(TAG, "Timeout waiting for data!");
+      return 0;
+    }
+    available = size_t(availi);
+    yield();
+  } while (bytes == 0 ? available == 0 : available < bytes);
+
+  if (bytes == 0)
+    bytes = std::min(available, size_t(1024));
+
+  bool success = false;
+  for (uint32_t i = 0; !success && i < 100; i++) {
+    int res = this->client_.read(buf, bytes);
+
+    if (res != int(bytes)) {
+      // ESP32 implementation has an issue where calling read can fail with EAGAIN (race condition)
+      // so just re-try it until it works (with generous timeout of 1s)
+      // because we check with available() first this should not cause us any trouble in all other cases
+      delay(10);
+    } else {
+      success = true;
+    }
+  }
+
+  if (!success) {
+    ESP_LOGW(TAG, "Reading %u bytes of binary data failed!", bytes);  // NOLINT
+    return 0;
+  }
+
+  return bytes;
+}
+
+void OTAComponent::set_auth_password(const std::string &password) { this->password_ = password; }
+
+float OTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
+uint16_t OTAComponent::get_port() const { return this->port_; }
+void OTAComponent::set_port(uint16_t port) { this->port_ = port; }
+bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
+  this->has_safe_mode_ = true;
+  this->safe_mode_start_time_ = millis();
+  this->safe_mode_enable_time_ = enable_time;
+  this->safe_mode_num_attempts_ = num_attempts;
+  this->rtc_ = global_preferences.make_preference<uint32_t>(233825507UL, false);
+  this->safe_mode_rtc_value_ = this->read_rtc_();
+
+  ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_);
+
+  if (this->safe_mode_rtc_value_ >= num_attempts) {
+    this->clean_rtc();
+
+    ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode.");
+
+    this->status_set_error();
+    this->set_timeout(enable_time, []() {
+      ESP_LOGE(TAG, "No OTA attempt made, restarting.");
+      App.reboot();
+    });
+
+    App.setup();
+
+    ESP_LOGI(TAG, "Waiting for OTA attempt.");
+
+    return true;
+  } else {
+    // increment counter
+    this->write_rtc_(this->safe_mode_rtc_value_ + 1);
+    return false;
+  }
+}
+void OTAComponent::write_rtc_(uint32_t val) { this->rtc_.save(&val); }
+uint32_t OTAComponent::read_rtc_() {
+  uint32_t val;
+  if (!this->rtc_.load(&val))
+    return 0;
+  return val;
+}
+void OTAComponent::clean_rtc() { this->write_rtc_(0); }
+void OTAComponent::on_safe_shutdown() {
+  if (this->has_safe_mode_)
+    this->clean_rtc();
+}
+
+}  // namespace ota
+}  // namespace esphome

+ 88 - 0
livingroom/src/esphome/components/ota/ota_component.h

@@ -0,0 +1,88 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/preferences.h"
+#include <WiFiServer.h>
+#include <WiFiClient.h>
+
+namespace esphome {
+namespace ota {
+
+enum OTAResponseTypes {
+  OTA_RESPONSE_OK = 0,
+  OTA_RESPONSE_REQUEST_AUTH = 1,
+
+  OTA_RESPONSE_HEADER_OK = 64,
+  OTA_RESPONSE_AUTH_OK = 65,
+  OTA_RESPONSE_UPDATE_PREPARE_OK = 66,
+  OTA_RESPONSE_BIN_MD5_OK = 67,
+  OTA_RESPONSE_RECEIVE_OK = 68,
+  OTA_RESPONSE_UPDATE_END_OK = 69,
+
+  OTA_RESPONSE_ERROR_MAGIC = 128,
+  OTA_RESPONSE_ERROR_UPDATE_PREPARE = 129,
+  OTA_RESPONSE_ERROR_AUTH_INVALID = 130,
+  OTA_RESPONSE_ERROR_WRITING_FLASH = 131,
+  OTA_RESPONSE_ERROR_UPDATE_END = 132,
+  OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 133,
+  OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 134,
+  OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 135,
+  OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 136,
+  OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137,
+  OTA_RESPONSE_ERROR_UNKNOWN = 255,
+};
+
+/// OTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
+class OTAComponent : public Component {
+ public:
+  /** Set a plaintext password that OTA will use for authentication.
+   *
+   * Warning: This password will be stored in plaintext in the ROM and can be read
+   * by intruders.
+   *
+   * @param password The plaintext password.
+   */
+  void set_auth_password(const std::string &password);
+
+  /// Manually set the port OTA should listen on.
+  void set_port(uint16_t port);
+
+  bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time);
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void loop() override;
+
+  uint16_t get_port() const;
+
+  void clean_rtc();
+
+  void on_safe_shutdown() override;
+
+ protected:
+  void write_rtc_(uint32_t val);
+  uint32_t read_rtc_();
+
+  void handle_();
+  size_t wait_receive_(uint8_t *buf, size_t bytes, bool check_disconnected = true);
+
+  std::string password_;
+
+  uint16_t port_;
+
+  WiFiServer *server_{nullptr};
+  WiFiClient client_{};
+
+  bool has_safe_mode_{false};              ///< stores whether safe mode can be enabled.
+  uint32_t safe_mode_start_time_;          ///< stores when safe mode was enabled.
+  uint32_t safe_mode_enable_time_{60000};  ///< The time safe mode should be on for.
+  uint32_t safe_mode_rtc_value_;
+  uint8_t safe_mode_num_attempts_;
+  ESPPreferenceObject rtc_;
+};
+
+}  // namespace ota
+}  // namespace esphome

+ 176 - 0
livingroom/src/esphome/components/sun/sun.cpp

@@ -0,0 +1,176 @@
+#include "sun.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sun {
+
+static const char *TAG = "sun";
+
+#undef PI
+
+/* Usually, ESPHome uses single-precision floating point values
+ * because those tend to be accurate enough and are more efficient.
+ *
+ * However, some of the data in this class has to be quite accurate, so double is
+ * used everywhere.
+ */
+static const double PI = 3.141592653589793;
+static const double TAU = 6.283185307179586;
+static const double TO_RADIANS = PI / 180.0;
+static const double TO_DEGREES = 180.0 / PI;
+static const double EARTH_TILT = 23.44 * TO_RADIANS;
+
+optional<time::ESPTime> Sun::sunrise(double elevation) {
+  auto time = this->time_->now();
+  if (!time.is_valid())
+    return {};
+  double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, true);
+  if (isnan(sun_time))
+    return {};
+  uint32_t epoch = this->calc_epoch_(time, sun_time);
+  return time::ESPTime::from_epoch_local(epoch);
+}
+optional<time::ESPTime> Sun::sunset(double elevation) {
+  auto time = this->time_->now();
+  if (!time.is_valid())
+    return {};
+  double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, false);
+  if (isnan(sun_time))
+    return {};
+  uint32_t epoch = this->calc_epoch_(time, sun_time);
+  return time::ESPTime::from_epoch_local(epoch);
+}
+double Sun::elevation() {
+  auto time = this->current_sun_time_();
+  if (isnan(time))
+    return NAN;
+  return this->elevation_(time);
+}
+double Sun::azimuth() {
+  auto time = this->current_sun_time_();
+  if (isnan(time))
+    return NAN;
+  return this->azimuth_(time);
+}
+// like clamp, but with doubles
+double clampd(double val, double min, double max) {
+  if (val < min)
+    return min;
+  if (val > max)
+    return max;
+  return val;
+}
+double Sun::sun_declination_(double sun_time) {
+  double n = sun_time - 1.0;
+  // maximum declination
+  const double tot = -sin(EARTH_TILT);
+
+  // eccentricity of the earth's orbit (ellipse)
+  double eccentricity = 0.0167;
+
+  // days since perihelion (January 3rd)
+  double days_since_perihelion = n - 2;
+  // days since december solstice (december 22)
+  double days_since_december_solstice = n + 10;
+  const double c = TAU / 365.24;
+  double v = cos(c * days_since_december_solstice + 2 * eccentricity * sin(c * days_since_perihelion));
+  // Make sure value is in range (double error may lead to results slightly larger than 1)
+  double x = clampd(tot * v, -1.0, 1.0);
+  return asin(x);
+}
+double Sun::elevation_ratio_(double sun_time) {
+  double decl = this->sun_declination_(sun_time);
+  double hangle = this->hour_angle_(sun_time);
+  double a = sin(this->latitude_rad_()) * sin(decl);
+  double b = cos(this->latitude_rad_()) * cos(decl) * cos(hangle);
+  double val = clampd(a + b, -1.0, 1.0);
+  return val;
+}
+double Sun::latitude_rad_() { return this->latitude_ * TO_RADIANS; }
+double Sun::hour_angle_(double sun_time) {
+  double time_of_day = fmod(sun_time, 1.0) * 24.0;
+  return -PI * (time_of_day - 12) / 12;
+}
+double Sun::elevation_(double sun_time) { return this->elevation_rad_(sun_time) * TO_DEGREES; }
+double Sun::elevation_rad_(double sun_time) { return asin(this->elevation_ratio_(sun_time)); }
+double Sun::zenith_rad_(double sun_time) { return acos(this->elevation_ratio_(sun_time)); }
+double Sun::azimuth_rad_(double sun_time) {
+  double hangle = -this->hour_angle_(sun_time);
+  double decl = this->sun_declination_(sun_time);
+  double zen = this->zenith_rad_(sun_time);
+  double nom = cos(zen) * sin(this->latitude_rad_()) - sin(decl);
+  double denom = sin(zen) * cos(this->latitude_rad_());
+  double v = clampd(nom / denom, -1.0, 1.0);
+  double az = PI - acos(v);
+  if (hangle > 0)
+    az = -az;
+  if (az < 0)
+    az += TAU;
+  return az;
+}
+double Sun::azimuth_(double sun_time) { return this->azimuth_rad_(sun_time) * TO_DEGREES; }
+double Sun::calc_sun_time_(const time::ESPTime &time) {
+  // Time as seen at 0° longitude
+  if (!time.is_valid())
+    return NAN;
+
+  double base = (time.day_of_year + time.hour / 24.0 + time.minute / 24.0 / 60.0 + time.second / 24.0 / 60.0 / 60.0);
+  // Add longitude correction
+  double add = this->longitude_ / 360.0;
+  return base + add;
+}
+uint32_t Sun::calc_epoch_(time::ESPTime base, double sun_time) {
+  sun_time -= this->longitude_ / 360.0;
+  base.day_of_year = uint32_t(floor(sun_time));
+
+  sun_time = (sun_time - base.day_of_year) * 24.0;
+  base.hour = uint32_t(floor(sun_time));
+
+  sun_time = (sun_time - base.hour) * 60.0;
+  base.minute = uint32_t(floor(sun_time));
+
+  sun_time = (sun_time - base.minute) * 60.0;
+  base.second = uint32_t(floor(sun_time));
+
+  base.recalc_timestamp_utc(true);
+  return base.timestamp;
+}
+double Sun::sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising) {
+  // Use binary search, newton's method would be better but binary search already
+  // converges quite well (19 cycles) and much simpler. Function is guaranteed to be
+  // monotonous.
+  double lo, hi;
+  if (rising) {
+    lo = day_of_year + 0.0;
+    hi = day_of_year + 0.5;
+  } else {
+    lo = day_of_year + 1.0;
+    hi = day_of_year + 0.5;
+  }
+
+  double min_elevation = this->elevation_(lo);
+  double max_elevation = this->elevation_(hi);
+  if (elevation < min_elevation || elevation > max_elevation)
+    return NAN;
+
+  // Accuracy: 0.1s
+  const double accuracy = 1.0 / (24.0 * 60.0 * 60.0 * 10.0);
+
+  while (fabs(hi - lo) > accuracy) {
+    double mid = (lo + hi) / 2.0;
+    double value = this->elevation_(mid) - elevation;
+    if (value < 0) {
+      lo = mid;
+    } else if (value > 0) {
+      hi = mid;
+    } else {
+      lo = hi = mid;
+      break;
+    }
+  }
+
+  return (lo + hi) / 2.0;
+}
+
+}  // namespace sun
+}  // namespace esphome

+ 137 - 0
livingroom/src/esphome/components/sun/sun.h

@@ -0,0 +1,137 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/time/real_time_clock.h"
+
+namespace esphome {
+namespace sun {
+
+class Sun {
+ public:
+  void set_time(time::RealTimeClock *time) { time_ = time; }
+  time::RealTimeClock *get_time() const { return time_; }
+  void set_latitude(double latitude) { latitude_ = latitude; }
+  void set_longitude(double longitude) { longitude_ = longitude; }
+
+  optional<time::ESPTime> sunrise(double elevation = 0.0);
+  optional<time::ESPTime> sunset(double elevation = 0.0);
+
+  double elevation();
+  double azimuth();
+
+ protected:
+  double current_sun_time_() { return this->calc_sun_time_(this->time_->utcnow()); }
+
+  /** Calculate the declination of the sun in rad.
+   *
+   * See https://en.wikipedia.org/wiki/Position_of_the_Sun#Declination_of_the_Sun_as_seen_from_Earth
+   *
+   * Accuracy: ±0.2°
+   *
+   * @param sun_time The day of the year, 1 means January 1st. See calc_sun_time_.
+   * @return Sun declination in degrees
+   */
+  double sun_declination_(double sun_time);
+
+  double elevation_ratio_(double sun_time);
+
+  /** Calculate the hour angle based on the sun time of day in hours.
+   *
+   * Positive in morning, 0 at noon, negative in afternoon.
+   *
+   * @param sun_time Sun time, see calc_sun_time_.
+   * @return Hour angle in rad.
+   */
+  double hour_angle_(double sun_time);
+
+  double elevation_(double sun_time);
+
+  double elevation_rad_(double sun_time);
+
+  double zenith_rad_(double sun_time);
+
+  double azimuth_rad_(double sun_time);
+
+  double azimuth_(double sun_time);
+
+  /** Return the sun time given by the time_ object.
+   *
+   * Sun time is defined as doubleing point day of year.
+   * Integer part encodes the day of the year (1=January 1st)
+   * Decimal part encodes time of day (1/24 = 1 hour)
+   */
+  double calc_sun_time_(const time::ESPTime &time);
+
+  uint32_t calc_epoch_(time::ESPTime base, double sun_time);
+
+  /** Calculate the sun time of day
+   *
+   * @param day_of_year
+   * @param elevation
+   * @param rising
+   * @return
+   */
+  double sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising);
+
+  double latitude_rad_();
+
+  time::RealTimeClock *time_;
+  /// Latitude in degrees, range: -90 to 90.
+  double latitude_;
+  /// Longitude in degrees, range: -180 to 180.
+  double longitude_;
+};
+
+class SunTrigger : public Trigger<>, public PollingComponent, public Parented<Sun> {
+ public:
+  SunTrigger() : PollingComponent(1000) {}
+
+  void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
+  void set_elevation(double elevation) { elevation_ = elevation; }
+
+  void update() override {
+    double current = this->parent_->elevation();
+    if (isnan(current))
+      return;
+
+    bool crossed;
+    if (this->sunrise_) {
+      crossed = this->last_elevation_ <= this->elevation_ && this->elevation_ < current;
+    } else {
+      crossed = this->last_elevation_ >= this->elevation_ && this->elevation_ > current;
+    }
+
+    if (crossed && !isnan(this->last_elevation_)) {
+      this->trigger();
+    }
+    this->last_elevation_ = current;
+  }
+
+ protected:
+  bool sunrise_;
+  double last_elevation_{NAN};
+  double elevation_;
+};
+
+template<typename... Ts> class SunCondition : public Condition<Ts...>, public Parented<Sun> {
+ public:
+  TEMPLATABLE_VALUE(double, elevation);
+  void set_above(bool above) { above_ = above; }
+
+  bool check(Ts... x) override {
+    double elevation = this->elevation_.value(x...);
+    double current = this->parent_->elevation();
+    if (this->above_)
+      return current > elevation;
+    else
+      return current < elevation;
+  }
+
+ protected:
+  bool above_;
+};
+
+}  // namespace sun
+}  // namespace esphome

+ 12 - 0
livingroom/src/esphome/components/sun/text_sensor/sun_text_sensor.cpp

@@ -0,0 +1,12 @@
+#include "sun_text_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sun {
+
+static const char *TAG = "sun.text_sensor";
+
+void SunTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sun Text Sensor", this); }
+
+}  // namespace sun
+}  // namespace esphome

+ 41 - 0
livingroom/src/esphome/components/sun/text_sensor/sun_text_sensor.h

@@ -0,0 +1,41 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sun/sun.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+
+namespace esphome {
+namespace sun {
+
+class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
+ public:
+  void set_parent(Sun *parent) { parent_ = parent; }
+  void set_elevation(double elevation) { elevation_ = elevation; }
+  void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
+  void set_format(const std::string &format) { format_ = format; }
+
+  void update() override {
+    optional<time::ESPTime> res;
+    if (this->sunrise_)
+      res = this->parent_->sunrise(this->elevation_);
+    else
+      res = this->parent_->sunset(this->elevation_);
+    if (!res) {
+      this->publish_state("");
+      return;
+    }
+
+    this->publish_state(res->strftime(this->format_));
+  }
+
+  void dump_config() override;
+
+ protected:
+  std::string format_{};
+  Sun *parent_;
+  double elevation_;
+  bool sunrise_;
+};
+
+}  // namespace sun
+}  // namespace esphome

+ 23 - 0
livingroom/src/esphome/components/template/text_sensor/template_text_sensor.cpp

@@ -0,0 +1,23 @@
+#include "template_text_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace template_ {
+
+static const char *TAG = "template.text_sensor";
+
+void TemplateTextSensor::update() {
+  if (!this->f_.has_value())
+    return;
+
+  auto val = (*this->f_)();
+  if (val.has_value()) {
+    this->publish_state(*val);
+  }
+}
+float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
+void TemplateTextSensor::set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; }
+void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); }
+
+}  // namespace template_
+}  // namespace esphome

+ 25 - 0
livingroom/src/esphome/components/template/text_sensor/template_text_sensor.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+
+namespace esphome {
+namespace template_ {
+
+class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent {
+ public:
+  void set_template(std::function<optional<std::string>()> &&f);
+
+  void update() override;
+
+  float get_setup_priority() const override;
+
+  void dump_config() override;
+
+ protected:
+  optional<std::function<optional<std::string>()>> f_{};
+};
+
+}  // namespace template_
+}  // namespace esphome

+ 41 - 0
livingroom/src/esphome/components/text_sensor/automation.h

@@ -0,0 +1,41 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+
+namespace esphome {
+namespace text_sensor {
+
+class TextSensorStateTrigger : public Trigger<std::string> {
+ public:
+  explicit TextSensorStateTrigger(TextSensor *parent) {
+    parent->add_on_state_callback([this](std::string value) { this->trigger(value); });
+  }
+};
+
+template<typename... Ts> class TextSensorStateCondition : public Condition<Ts...> {
+ public:
+  explicit TextSensorStateCondition(TextSensor *parent) : parent_(parent) {}
+
+  TEMPLATABLE_VALUE(std::string, state)
+
+  bool check(Ts... x) override { return this->parent_->state == this->state_.value(x...); }
+
+ protected:
+  TextSensor *parent_;
+};
+
+template<typename... Ts> class TextSensorPublishAction : public Action<Ts...> {
+ public:
+  TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {}
+  TEMPLATABLE_VALUE(std::string, state)
+
+  void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); }
+
+ protected:
+  TextSensor *sensor_;
+};
+
+}  // namespace text_sensor
+}  // namespace esphome

+ 33 - 0
livingroom/src/esphome/components/text_sensor/text_sensor.cpp

@@ -0,0 +1,33 @@
+#include "text_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace text_sensor {
+
+static const char *TAG = "text_sensor";
+
+TextSensor::TextSensor() : TextSensor("") {}
+TextSensor::TextSensor(const std::string &name) : Nameable(name) {}
+
+void TextSensor::publish_state(std::string state) {
+  this->state = state;
+  this->has_state_ = true;
+  ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str());
+  this->callback_.call(state);
+}
+void TextSensor::set_icon(const std::string &icon) { this->icon_ = icon; }
+void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
+  this->callback_.add(std::move(callback));
+}
+std::string TextSensor::get_icon() {
+  if (this->icon_.has_value())
+    return *this->icon_;
+  return this->icon();
+}
+std::string TextSensor::icon() { return ""; }
+std::string TextSensor::unique_id() { return ""; }
+bool TextSensor::has_state() { return this->has_state_; }
+uint32_t TextSensor::hash_base() { return 334300109UL; }
+
+}  // namespace text_sensor
+}  // namespace esphome

+ 52 - 0
livingroom/src/esphome/components/text_sensor/text_sensor.h

@@ -0,0 +1,52 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace text_sensor {
+
+#define LOG_TEXT_SENSOR(prefix, type, obj) \
+  if (obj != nullptr) { \
+    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \
+    if (!obj->get_icon().empty()) { \
+      ESP_LOGCONFIG(TAG, "%s  Icon: '%s'", prefix, obj->get_icon().c_str()); \
+    } \
+    if (!obj->unique_id().empty()) { \
+      ESP_LOGV(TAG, "%s  Unique ID: '%s'", prefix, obj->unique_id().c_str()); \
+    } \
+  }
+
+class TextSensor : public Nameable {
+ public:
+  explicit TextSensor();
+  explicit TextSensor(const std::string &name);
+
+  void publish_state(std::string state);
+
+  void set_icon(const std::string &icon);
+
+  void add_on_state_callback(std::function<void(std::string)> callback);
+
+  std::string state;
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  std::string get_icon();
+
+  virtual std::string icon();
+
+  virtual std::string unique_id();
+
+  bool has_state();
+
+ protected:
+  uint32_t hash_base() override;
+
+  CallbackManager<void(std::string)> callback_;
+  optional<std::string> icon_;
+  bool has_state_{false};
+};
+
+}  // namespace text_sensor
+}  // namespace esphome

+ 83 - 0
livingroom/src/esphome/components/time/automation.cpp

@@ -0,0 +1,83 @@
+#include "automation.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace time {
+
+static const char *TAG = "automation";
+
+void CronTrigger::add_second(uint8_t second) { this->seconds_[second] = true; }
+void CronTrigger::add_minute(uint8_t minute) { this->minutes_[minute] = true; }
+void CronTrigger::add_hour(uint8_t hour) { this->hours_[hour] = true; }
+void CronTrigger::add_day_of_month(uint8_t day_of_month) { this->days_of_month_[day_of_month] = true; }
+void CronTrigger::add_month(uint8_t month) { this->months_[month] = true; }
+void CronTrigger::add_day_of_week(uint8_t day_of_week) { this->days_of_week_[day_of_week] = true; }
+bool CronTrigger::matches(const ESPTime &time) {
+  return time.is_valid() && this->seconds_[time.second] && this->minutes_[time.minute] && this->hours_[time.hour] &&
+         this->days_of_month_[time.day_of_month] && this->months_[time.month] && this->days_of_week_[time.day_of_week];
+}
+void CronTrigger::loop() {
+  ESPTime time = this->rtc_->now();
+  if (!time.is_valid())
+    return;
+
+  if (this->last_check_.has_value()) {
+    if (*this->last_check_ >= time) {
+      // already handled this one
+      return;
+    }
+
+    while (true) {
+      this->last_check_->increment_second();
+      if (*this->last_check_ >= time)
+        break;
+
+      if (this->matches(*this->last_check_))
+        this->trigger();
+    }
+  }
+
+  this->last_check_ = time;
+  if (!time.fields_in_range()) {
+    ESP_LOGW(TAG, "Time is out of range!");
+    ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u DayOfWeek=%u DayOfMonth=%u DayOfYear=%u Month=%u time=%ld",
+             time.second, time.minute, time.hour, time.day_of_week, time.day_of_month, time.day_of_year, time.month,
+             time.timestamp);
+  }
+
+  if (this->matches(time))
+    this->trigger();
+}
+CronTrigger::CronTrigger(RealTimeClock *rtc) : rtc_(rtc) {}
+void CronTrigger::add_seconds(const std::vector<uint8_t> &seconds) {
+  for (uint8_t it : seconds)
+    this->add_second(it);
+}
+void CronTrigger::add_minutes(const std::vector<uint8_t> &minutes) {
+  for (uint8_t it : minutes)
+    this->add_minute(it);
+}
+void CronTrigger::add_hours(const std::vector<uint8_t> &hours) {
+  for (uint8_t it : hours)
+    this->add_hour(it);
+}
+void CronTrigger::add_days_of_month(const std::vector<uint8_t> &days_of_month) {
+  for (uint8_t it : days_of_month)
+    this->add_day_of_month(it);
+}
+void CronTrigger::add_months(const std::vector<uint8_t> &months) {
+  for (uint8_t it : months)
+    this->add_month(it);
+}
+void CronTrigger::add_days_of_week(const std::vector<uint8_t> &days_of_week) {
+  for (uint8_t it : days_of_week)
+    this->add_day_of_week(it);
+}
+float CronTrigger::get_setup_priority() const { return setup_priority::HARDWARE; }
+
+SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) {
+  rtc->add_on_time_sync_callback([this]() { this->trigger(); });
+}
+
+}  // namespace time
+}  // namespace esphome

+ 48 - 0
livingroom/src/esphome/components/time/automation.h

@@ -0,0 +1,48 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "real_time_clock.h"
+
+namespace esphome {
+namespace time {
+
+class CronTrigger : public Trigger<>, public Component {
+ public:
+  explicit CronTrigger(RealTimeClock *rtc);
+  void add_second(uint8_t second);
+  void add_seconds(const std::vector<uint8_t> &seconds);
+  void add_minute(uint8_t minute);
+  void add_minutes(const std::vector<uint8_t> &minutes);
+  void add_hour(uint8_t hour);
+  void add_hours(const std::vector<uint8_t> &hours);
+  void add_day_of_month(uint8_t day_of_month);
+  void add_days_of_month(const std::vector<uint8_t> &days_of_month);
+  void add_month(uint8_t month);
+  void add_months(const std::vector<uint8_t> &months);
+  void add_day_of_week(uint8_t day_of_week);
+  void add_days_of_week(const std::vector<uint8_t> &days_of_week);
+  bool matches(const ESPTime &time);
+  void loop() override;
+  float get_setup_priority() const override;
+
+ protected:
+  std::bitset<61> seconds_;
+  std::bitset<60> minutes_;
+  std::bitset<24> hours_;
+  std::bitset<32> days_of_month_;
+  std::bitset<13> months_;
+  std::bitset<8> days_of_week_;
+  RealTimeClock *rtc_;
+  optional<ESPTime> last_check_;
+};
+
+class SyncTrigger : public Trigger<>, public Component {
+ public:
+  explicit SyncTrigger(RealTimeClock *rtc);
+
+ protected:
+  RealTimeClock *rtc_;
+};
+}  // namespace time
+}  // namespace esphome

+ 170 - 0
livingroom/src/esphome/components/time/real_time_clock.cpp

@@ -0,0 +1,170 @@
+#include "real_time_clock.h"
+#include "esphome/core/log.h"
+#include "lwip/opt.h"
+#ifdef ARDUINO_ARCH_ESP8266
+#include "sys/time.h"
+#endif
+#include "errno.h"
+
+namespace esphome {
+namespace time {
+
+static const char *TAG = "time";
+
+RealTimeClock::RealTimeClock() = default;
+void RealTimeClock::call_setup() {
+  setenv("TZ", this->timezone_.c_str(), 1);
+  tzset();
+  PollingComponent::call_setup();
+}
+void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
+  struct timeval timev {
+    .tv_sec = static_cast<time_t>(epoch), .tv_usec = 0,
+  };
+  ESP_LOGVV(TAG, "Got epoch %u", epoch);
+  timezone tz = {0, 0};
+  int ret = settimeofday(&timev, &tz);
+  if (ret == EINVAL) {
+    // Some ESP8266 frameworks abort when timezone parameter is not NULL
+    // while ESP32 expects it not to be NULL
+    ret = settimeofday(&timev, nullptr);
+  }
+
+  if (ret != 0) {
+    ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
+  }
+
+  auto time = this->now();
+  char buf[128];
+  time.strftime(buf, sizeof(buf), "%c");
+  ESP_LOGD(TAG, "Synchronized time: %s", buf);
+
+  this->time_sync_callback_.call();
+}
+
+size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) {
+  struct tm c_tm = this->to_c_tm();
+  return ::strftime(buffer, buffer_len, format, &c_tm);
+}
+ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) {
+  ESPTime res{};
+  res.second = uint8_t(c_tm->tm_sec);
+  res.minute = uint8_t(c_tm->tm_min);
+  res.hour = uint8_t(c_tm->tm_hour);
+  res.day_of_week = uint8_t(c_tm->tm_wday + 1);
+  res.day_of_month = uint8_t(c_tm->tm_mday);
+  res.day_of_year = uint16_t(c_tm->tm_yday + 1);
+  res.month = uint8_t(c_tm->tm_mon + 1);
+  res.year = uint16_t(c_tm->tm_year + 1900);
+  res.is_dst = bool(c_tm->tm_isdst);
+  res.timestamp = c_time;
+  return res;
+}
+struct tm ESPTime::to_c_tm() {
+  struct tm c_tm {};
+  c_tm.tm_sec = this->second;
+  c_tm.tm_min = this->minute;
+  c_tm.tm_hour = this->hour;
+  c_tm.tm_mday = this->day_of_month;
+  c_tm.tm_mon = this->month - 1;
+  c_tm.tm_year = this->year - 1900;
+  c_tm.tm_wday = this->day_of_week - 1;
+  c_tm.tm_yday = this->day_of_year - 1;
+  c_tm.tm_isdst = this->is_dst;
+  return c_tm;
+}
+std::string ESPTime::strftime(const std::string &format) {
+  std::string timestr;
+  timestr.resize(format.size() * 4);
+  struct tm c_tm = this->to_c_tm();
+  size_t len = ::strftime(&timestr[0], timestr.size(), format.c_str(), &c_tm);
+  while (len == 0) {
+    timestr.resize(timestr.size() * 2);
+    len = ::strftime(&timestr[0], timestr.size(), format.c_str(), &c_tm);
+  }
+  timestr.resize(len);
+  return timestr;
+}
+
+template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end) {
+  current++;
+  if (current >= end) {
+    current = begin;
+    return true;
+  }
+  return false;
+}
+
+static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); }
+
+static uint8_t days_in_month(uint8_t month, uint16_t year) {
+  static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+  uint8_t days = DAYS_IN_MONTH[month];
+  if (month == 2 && is_leap_year(year))
+    return 29;
+  return days;
+}
+
+void ESPTime::increment_second() {
+  this->timestamp++;
+  if (!increment_time_value(this->second, 0, 60))
+    return;
+
+  // second roll-over, increment minute
+  if (!increment_time_value(this->minute, 0, 60))
+    return;
+
+  // minute roll-over, increment hour
+  if (!increment_time_value(this->hour, 0, 24))
+    return;
+
+  // hour roll-over, increment day
+  increment_time_value(this->day_of_week, 1, 8);
+
+  if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) {
+    // day of month roll-over, increment month
+    increment_time_value(this->month, 1, 13);
+  }
+
+  uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365;
+  if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) {
+    // day of year roll-over, increment year
+    this->year++;
+  }
+}
+void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
+  time_t res = 0;
+
+  if (!this->fields_in_range()) {
+    this->timestamp = -1;
+    return;
+  }
+
+  for (int i = 1970; i < this->year; i++)
+    res += is_leap_year(i) ? 366 : 365;
+
+  if (use_day_of_year) {
+    res += this->day_of_year - 1;
+  } else {
+    for (int i = 1; i < this->month; i++)
+      res += days_in_month(i, this->year);
+
+    res += this->day_of_month - 1;
+  }
+
+  res *= 24;
+  res += this->hour;
+  res *= 60;
+  res += this->minute;
+  res *= 60;
+  res += this->second;
+  this->timestamp = res;
+}
+bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; }
+bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; }
+bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; }
+bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; }
+bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; }
+
+}  // namespace time
+}  // namespace esphome

+ 153 - 0
livingroom/src/esphome/components/time/real_time_clock.h

@@ -0,0 +1,153 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/automation.h"
+#include <stdlib.h>
+#include <time.h>
+#include <bitset>
+
+namespace esphome {
+namespace time {
+
+/// A more user-friendly version of struct tm from time.h
+struct ESPTime {
+  /** seconds after the minute [0-60]
+   * @note second is generally 0-59; the extra range is to accommodate leap seconds.
+   */
+  uint8_t second;
+  /// minutes after the hour [0-59]
+  uint8_t minute;
+  /// hours since midnight [0-23]
+  uint8_t hour;
+  /// day of the week; sunday=1 [1-7]
+  uint8_t day_of_week;
+  /// day of the month [1-31]
+  uint8_t day_of_month;
+  /// day of the year [1-366]
+  uint16_t day_of_year;
+  /// month; january=1 [1-12]
+  uint8_t month;
+  /// year
+  uint16_t year;
+  /// daylight savings time flag
+  bool is_dst;
+  union {
+    ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time;
+    /// unix epoch time (seconds since UTC Midnight January 1, 1970)
+    time_t timestamp;
+  };
+
+  /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument.
+   * Up to buffer_len bytes are written.
+   *
+   * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime
+   */
+  size_t strftime(char *buffer, size_t buffer_len, const char *format);
+
+  /** Convert this ESPTime struct to a string as specified by the format argument.
+   * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime
+   *
+   * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some
+   * microcontrollers.
+   */
+  std::string strftime(const std::string &format);
+
+  /// Check if this ESPTime is valid (all fields in range and year is greater than 2018)
+  bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); }
+
+  /// Check if all time fields of this ESPTime are in range.
+  bool fields_in_range() const {
+    return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 &&
+           this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 &&
+           this->day_of_year < 367 && this->month > 0 && this->month < 13;
+  }
+
+  /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance.
+  static ESPTime from_c_tm(struct tm *c_tm, time_t c_time);
+
+  /** Convert an UTC epoch timestamp to a local time ESPTime instance.
+   *
+   * @param epoch Seconds since 1st January 1970. In UTC.
+   * @return The generated ESPTime
+   */
+  static ESPTime from_epoch_local(time_t epoch) {
+    struct tm *c_tm = ::localtime(&epoch);
+    return ESPTime::from_c_tm(c_tm, epoch);
+  }
+  /** Convert an UTC epoch timestamp to a UTC time ESPTime instance.
+   *
+   * @param epoch Seconds since 1st January 1970. In UTC.
+   * @return The generated ESPTime
+   */
+  static ESPTime from_epoch_utc(time_t epoch) {
+    struct tm *c_tm = ::gmtime(&epoch);
+    return ESPTime::from_c_tm(c_tm, epoch);
+  }
+
+  /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC).
+  void recalc_timestamp_utc(bool use_day_of_year = true);
+
+  /// Convert this ESPTime instance back to a tm struct.
+  struct tm to_c_tm();
+
+  /// Increment this clock instance by one second.
+  void increment_second();
+  bool operator<(ESPTime other);
+  bool operator<=(ESPTime other);
+  bool operator==(ESPTime other);
+  bool operator>=(ESPTime other);
+  bool operator>(ESPTime other);
+};
+
+/// The RealTimeClock class exposes common timekeeping functions via the device's local real-time clock.
+///
+/// \note
+/// The C library (newlib) available on ESPs only supports TZ strings that specify an offset and DST info;
+/// you cannot specify zone names or paths to zoneinfo files.
+/// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+class RealTimeClock : public PollingComponent {
+ public:
+  explicit RealTimeClock();
+
+  /// Set the time zone.
+  void set_timezone(const std::string &tz) { this->timezone_ = tz; }
+
+  /// Get the time zone currently in use.
+  std::string get_timezone() { return this->timezone_; }
+
+  /// Get the time in the currently defined timezone.
+  ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); }
+
+  /// Get the time without any time zone or DST corrections.
+  ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
+
+  /// Get the current time as the UTC epoch since January 1st 1970.
+  time_t timestamp_now() { return ::time(nullptr); }
+
+  void call_setup() override;
+
+  void add_on_time_sync_callback(std::function<void()> callback) {
+    this->time_sync_callback_.add(std::move(callback));
+  };
+
+ protected:
+  /// Report a unix epoch as current time.
+  void synchronize_epoch_(uint32_t epoch);
+
+  std::string timezone_{};
+
+  CallbackManager<void()> time_sync_callback_;
+};
+
+template<typename... Ts> class TimeHasTimeCondition : public Condition<Ts...> {
+ public:
+  TimeHasTimeCondition(RealTimeClock *parent) : parent_(parent) {}
+  bool check(Ts... x) override { return this->parent_->now().is_valid(); }
+
+ protected:
+  RealTimeClock *parent_;
+};
+
+}  // namespace time
+}  // namespace esphome

+ 24 - 0
livingroom/src/esphome/components/version/version_text_sensor.cpp

@@ -0,0 +1,24 @@
+#include "version_text_sensor.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/core/version.h"
+
+namespace esphome {
+namespace version {
+
+static const char *TAG = "version.text_sensor";
+
+void VersionTextSensor::setup() {
+  if (this->hide_timestamp_) {
+    this->publish_state(ESPHOME_VERSION);
+  } else {
+    this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time());
+  }
+}
+float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; }
+void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; }
+std::string VersionTextSensor::unique_id() { return get_mac_address() + "-version"; }
+void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); }
+
+}  // namespace version
+}  // namespace esphome

+ 22 - 0
livingroom/src/esphome/components/version/version_text_sensor.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+
+namespace esphome {
+namespace version {
+
+class VersionTextSensor : public text_sensor::TextSensor, public Component {
+ public:
+  void set_hide_timestamp(bool hide_timestamp);
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  std::string unique_id() override;
+
+ protected:
+  bool hide_timestamp_{false};
+};
+
+}  // namespace version
+}  // namespace esphome

+ 704 - 0
livingroom/src/esphome/components/web_server/web_server.cpp

@@ -0,0 +1,704 @@
+#include "web_server.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/core/util.h"
+#include "esphome/components/json/json_util.h"
+
+#include "StreamString.h"
+
+#include <cstdlib>
+
+#ifdef USE_LOGGER
+#include <esphome/components/logger/logger.h>
+#endif
+
+namespace esphome {
+namespace web_server {
+
+static const char *TAG = "web_server";
+
+void write_row(AsyncResponseStream *stream, Nameable *obj, const std::string &klass, const std::string &action) {
+  if (obj->is_internal())
+    return;
+  stream->print("<tr class=\"");
+  stream->print(klass.c_str());
+  stream->print("\" id=\"");
+  stream->print(klass.c_str());
+  stream->print("-");
+  stream->print(obj->get_object_id().c_str());
+  stream->print("\"><td>");
+  stream->print(obj->get_name().c_str());
+  stream->print("</td><td></td><td>");
+  stream->print(action.c_str());
+  stream->print("</td>");
+  stream->print("</tr>");
+}
+
+UrlMatch match_url(const std::string &url, bool only_domain = false) {
+  UrlMatch match;
+  match.valid = false;
+  size_t domain_end = url.find('/', 1);
+  if (domain_end == std::string::npos)
+    return match;
+  match.domain = url.substr(1, domain_end - 1);
+  if (only_domain) {
+    match.valid = true;
+    return match;
+  }
+  if (url.length() == domain_end - 1)
+    return match;
+  size_t id_begin = domain_end + 1;
+  size_t id_end = url.find('/', id_begin);
+  match.valid = true;
+  if (id_end == std::string::npos) {
+    match.id = url.substr(id_begin, url.length() - id_begin);
+    return match;
+  }
+  match.id = url.substr(id_begin, id_end - id_begin);
+  size_t method_begin = id_end + 1;
+  match.method = url.substr(method_begin, url.length() - method_begin);
+  return match;
+}
+
+void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; }
+void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
+void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
+void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
+
+void WebServer::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up web server...");
+  this->setup_controller();
+  this->base_->init();
+
+  this->events_.onConnect([this](AsyncEventSourceClient *client) {
+    // Configure reconnect timeout
+    client->send("", "ping", millis(), 30000);
+
+#ifdef USE_SENSOR
+    for (auto *obj : App.get_sensors())
+      if (!obj->is_internal())
+        client->send(this->sensor_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_SWITCH
+    for (auto *obj : App.get_switches())
+      if (!obj->is_internal())
+        client->send(this->switch_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_BINARY_SENSOR
+    for (auto *obj : App.get_binary_sensors())
+      if (!obj->is_internal())
+        client->send(this->binary_sensor_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_FAN
+    for (auto *obj : App.get_fans())
+      if (!obj->is_internal())
+        client->send(this->fan_json(obj).c_str(), "state");
+#endif
+
+#ifdef USE_LIGHT
+    for (auto *obj : App.get_lights())
+      if (!obj->is_internal())
+        client->send(this->light_json(obj).c_str(), "state");
+#endif
+
+#ifdef USE_TEXT_SENSOR
+    for (auto *obj : App.get_text_sensors())
+      if (!obj->is_internal())
+        client->send(this->text_sensor_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_COVER
+    for (auto *obj : App.get_covers())
+      if (!obj->is_internal())
+        client->send(this->cover_json(obj).c_str(), "state");
+#endif
+  });
+
+#ifdef USE_LOGGER
+  if (logger::global_logger != nullptr)
+    logger::global_logger->add_on_log_callback(
+        [this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); });
+#endif
+  this->base_->add_handler(&this->events_);
+  this->base_->add_handler(this);
+  this->base_->add_ota_handler();
+
+  this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); });
+}
+void WebServer::dump_config() {
+  ESP_LOGCONFIG(TAG, "Web Server:");
+  ESP_LOGCONFIG(TAG, "  Address: %s:%u", network_get_address().c_str(), this->base_->get_port());
+  if (this->using_auth()) {
+    ESP_LOGCONFIG(TAG, "  Basic authentication enabled");
+  }
+}
+float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
+
+void WebServer::handle_index_request(AsyncWebServerRequest *request) {
+  AsyncResponseStream *stream = request->beginResponseStream("text/html");
+  std::string title = App.get_name() + " Web Server";
+  stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><title>"));
+  stream->print(title.c_str());
+  stream->print(F("</title>"));
+#ifdef WEBSERVER_CSS_INCLUDE
+  stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">"));
+#endif
+  if (strlen(this->css_url_) > 0) {
+    stream->print(F("<link rel=\"stylesheet\" href=\""));
+    stream->print(this->css_url_);
+    stream->print(F("\">"));
+  }
+  stream->print(F("</head><body><article class=\"markdown-body\"><h1>"));
+  stream->print(title.c_str());
+  stream->print(F("</h1><h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>"));
+  // All content is controlled and created by user - so allowing all origins is fine here.
+  stream->addHeader("Access-Control-Allow-Origin", "*");
+
+#ifdef USE_SENSOR
+  for (auto *obj : App.get_sensors())
+    write_row(stream, obj, "sensor", "");
+#endif
+
+#ifdef USE_SWITCH
+  for (auto *obj : App.get_switches())
+    write_row(stream, obj, "switch", "<button>Toggle</button>");
+#endif
+
+#ifdef USE_BINARY_SENSOR
+  for (auto *obj : App.get_binary_sensors())
+    write_row(stream, obj, "binary_sensor", "");
+#endif
+
+#ifdef USE_FAN
+  for (auto *obj : App.get_fans())
+    write_row(stream, obj, "fan", "<button>Toggle</button>");
+#endif
+
+#ifdef USE_LIGHT
+  for (auto *obj : App.get_lights())
+    write_row(stream, obj, "light", "<button>Toggle</button>");
+#endif
+
+#ifdef USE_TEXT_SENSOR
+  for (auto *obj : App.get_text_sensors())
+    write_row(stream, obj, "text_sensor", "");
+#endif
+
+#ifdef USE_COVER
+  for (auto *obj : App.get_covers())
+    write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>");
+#endif
+
+  stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
+                  "REST API documentation.</p>"
+                  "<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
+                  "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"
+                  "<h2>Debug Log</h2><pre id=\"log\"></pre>"));
+#ifdef WEBSERVER_JS_INCLUDE
+  if (this->js_include_ != nullptr) {
+    stream->print(F("<script src=\"/0.js\"></script>"));
+  }
+#endif
+  if (strlen(this->js_url_) > 0) {
+    stream->print(F("<script src=\""));
+    stream->print(this->js_url_);
+    stream->print(F("\"></script>"));
+  }
+  stream->print(F("</article></body></html>"));
+
+  request->send(stream);
+}
+
+#ifdef WEBSERVER_CSS_INCLUDE
+void WebServer::handle_css_request(AsyncWebServerRequest *request) {
+  AsyncResponseStream *stream = request->beginResponseStream("text/css");
+  if (this->css_include_ != nullptr) {
+    stream->print(this->css_include_);
+  }
+
+  request->send(stream);
+}
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+void WebServer::handle_js_request(AsyncWebServerRequest *request) {
+  AsyncResponseStream *stream = request->beginResponseStream("text/javascript");
+  if (this->js_include_ != nullptr) {
+    stream->print(this->js_include_);
+  }
+
+  request->send(stream);
+}
+#endif
+
+#ifdef USE_SENSOR
+void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
+  this->events_.send(this->sensor_json(obj, state).c_str(), "state");
+}
+void WebServer::handle_sensor_request(AsyncWebServerRequest *request, UrlMatch match) {
+  for (sensor::Sensor *obj : App.get_sensors()) {
+    if (obj->is_internal())
+      continue;
+    if (obj->get_object_id() != match.id)
+      continue;
+    std::string data = this->sensor_json(obj, obj->state);
+    request->send(200, "text/json", data.c_str());
+    return;
+  }
+  request->send(404);
+}
+std::string WebServer::sensor_json(sensor::Sensor *obj, float value) {
+  return json::build_json([obj, value](JsonObject &root) {
+    root["id"] = "sensor-" + obj->get_object_id();
+    std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals());
+    if (!obj->get_unit_of_measurement().empty())
+      state += " " + obj->get_unit_of_measurement();
+    root["state"] = state;
+    root["value"] = value;
+  });
+}
+#endif
+
+#ifdef USE_TEXT_SENSOR
+void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) {
+  this->events_.send(this->text_sensor_json(obj, state).c_str(), "state");
+}
+void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, UrlMatch match) {
+  for (text_sensor::TextSensor *obj : App.get_text_sensors()) {
+    if (obj->is_internal())
+      continue;
+    if (obj->get_object_id() != match.id)
+      continue;
+    std::string data = this->text_sensor_json(obj, obj->state);
+    request->send(200, "text/json", data.c_str());
+    return;
+  }
+  request->send(404);
+}
+std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value) {
+  return json::build_json([obj, value](JsonObject &root) {
+    root["id"] = "text_sensor-" + obj->get_object_id();
+    root["state"] = value;
+    root["value"] = value;
+  });
+}
+#endif
+
+#ifdef USE_SWITCH
+void WebServer::on_switch_update(switch_::Switch *obj, bool state) {
+  this->events_.send(this->switch_json(obj, state).c_str(), "state");
+}
+std::string WebServer::switch_json(switch_::Switch *obj, bool value) {
+  return json::build_json([obj, value](JsonObject &root) {
+    root["id"] = "switch-" + obj->get_object_id();
+    root["state"] = value ? "ON" : "OFF";
+    root["value"] = value;
+  });
+}
+void WebServer::handle_switch_request(AsyncWebServerRequest *request, UrlMatch match) {
+  for (switch_::Switch *obj : App.get_switches()) {
+    if (obj->is_internal())
+      continue;
+    if (obj->get_object_id() != match.id)
+      continue;
+
+    if (request->method() == HTTP_GET) {
+      std::string data = this->switch_json(obj, obj->state);
+      request->send(200, "text/json", data.c_str());
+    } else if (match.method == "toggle") {
+      this->defer([obj]() { obj->toggle(); });
+      request->send(200);
+    } else if (match.method == "turn_on") {
+      this->defer([obj]() { obj->turn_on(); });
+      request->send(200);
+    } else if (match.method == "turn_off") {
+      this->defer([obj]() { obj->turn_off(); });
+      request->send(200);
+    } else {
+      request->send(404);
+    }
+    return;
+  }
+  request->send(404);
+}
+#endif
+
+#ifdef USE_BINARY_SENSOR
+void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
+  if (obj->is_internal())
+    return;
+  this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state");
+}
+std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value) {
+  return json::build_json([obj, value](JsonObject &root) {
+    root["id"] = "binary_sensor-" + obj->get_object_id();
+    root["state"] = value ? "ON" : "OFF";
+    root["value"] = value;
+  });
+}
+void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, UrlMatch match) {
+  for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) {
+    if (obj->is_internal())
+      continue;
+    if (obj->get_object_id() != match.id)
+      continue;
+    std::string data = this->binary_sensor_json(obj, obj->state);
+    request->send(200, "text/json", data.c_str());
+    return;
+  }
+  request->send(404);
+}
+#endif
+
+#ifdef USE_FAN
+void WebServer::on_fan_update(fan::FanState *obj) {
+  if (obj->is_internal())
+    return;
+  this->events_.send(this->fan_json(obj).c_str(), "state");
+}
+std::string WebServer::fan_json(fan::FanState *obj) {
+  return json::build_json([obj](JsonObject &root) {
+    root["id"] = "fan-" + obj->get_object_id();
+    root["state"] = obj->state ? "ON" : "OFF";
+    root["value"] = obj->state;
+    if (obj->get_traits().supports_speed()) {
+      switch (obj->speed) {
+        case fan::FAN_SPEED_LOW:
+          root["speed"] = "low";
+          break;
+        case fan::FAN_SPEED_MEDIUM:
+          root["speed"] = "medium";
+          break;
+        case fan::FAN_SPEED_HIGH:
+          root["speed"] = "high";
+          break;
+      }
+    }
+    if (obj->get_traits().supports_oscillation())
+      root["oscillation"] = obj->oscillating;
+  });
+}
+void WebServer::handle_fan_request(AsyncWebServerRequest *request, UrlMatch match) {
+  for (fan::FanState *obj : App.get_fans()) {
+    if (obj->is_internal())
+      continue;
+    if (obj->get_object_id() != match.id)
+      continue;
+
+    if (request->method() == HTTP_GET) {
+      std::string data = this->fan_json(obj);
+      request->send(200, "text/json", data.c_str());
+    } else if (match.method == "toggle") {
+      this->defer([obj]() { obj->toggle().perform(); });
+      request->send(200);
+    } else if (match.method == "turn_on") {
+      auto call = obj->turn_on();
+      if (request->hasParam("speed")) {
+        String speed = request->getParam("speed")->value();
+        call.set_speed(speed.c_str());
+      }
+      if (request->hasParam("oscillation")) {
+        String speed = request->getParam("oscillation")->value();
+        auto val = parse_on_off(speed.c_str());
+        switch (val) {
+          case PARSE_ON:
+            call.set_oscillating(true);
+            break;
+          case PARSE_OFF:
+            call.set_oscillating(false);
+            break;
+          case PARSE_TOGGLE:
+            call.set_oscillating(!obj->oscillating);
+            break;
+          case PARSE_NONE:
+            request->send(404);
+            return;
+        }
+      }
+      this->defer([call]() { call.perform(); });
+      request->send(200);
+    } else if (match.method == "turn_off") {
+      this->defer([obj]() { obj->turn_off().perform(); });
+      request->send(200);
+    } else {
+      request->send(404);
+    }
+    return;
+  }
+  request->send(404);
+}
+#endif
+
+#ifdef USE_LIGHT
+void WebServer::on_light_update(light::LightState *obj) {
+  if (obj->is_internal())
+    return;
+  this->events_.send(this->light_json(obj).c_str(), "state");
+}
+void WebServer::handle_light_request(AsyncWebServerRequest *request, UrlMatch match) {
+  for (light::LightState *obj : App.get_lights()) {
+    if (obj->is_internal())
+      continue;
+    if (obj->get_object_id() != match.id)
+      continue;
+
+    if (request->method() == HTTP_GET) {
+      std::string data = this->light_json(obj);
+      request->send(200, "text/json", data.c_str());
+    } else if (match.method == "toggle") {
+      this->defer([obj]() { obj->toggle().perform(); });
+      request->send(200);
+    } else if (match.method == "turn_on") {
+      auto call = obj->turn_on();
+      if (request->hasParam("brightness"))
+        call.set_brightness(request->getParam("brightness")->value().toFloat() / 255.0f);
+      if (request->hasParam("r"))
+        call.set_red(request->getParam("r")->value().toFloat() / 255.0f);
+      if (request->hasParam("g"))
+        call.set_green(request->getParam("g")->value().toFloat() / 255.0f);
+      if (request->hasParam("b"))
+        call.set_blue(request->getParam("b")->value().toFloat() / 255.0f);
+      if (request->hasParam("white_value"))
+        call.set_white(request->getParam("white_value")->value().toFloat() / 255.0f);
+      if (request->hasParam("color_temp"))
+        call.set_color_temperature(request->getParam("color_temp")->value().toFloat());
+
+      if (request->hasParam("flash")) {
+        float length_s = request->getParam("flash")->value().toFloat();
+        call.set_flash_length(static_cast<uint32_t>(length_s * 1000));
+      }
+
+      if (request->hasParam("transition")) {
+        float length_s = request->getParam("transition")->value().toFloat();
+        call.set_transition_length(static_cast<uint32_t>(length_s * 1000));
+      }
+
+      if (request->hasParam("effect")) {
+        const char *effect = request->getParam("effect")->value().c_str();
+        call.set_effect(effect);
+      }
+
+      this->defer([call]() mutable { call.perform(); });
+      request->send(200);
+    } else if (match.method == "turn_off") {
+      auto call = obj->turn_off();
+      if (request->hasParam("transition")) {
+        auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000;
+        call.set_transition_length(length);
+      }
+      this->defer([call]() mutable { call.perform(); });
+      request->send(200);
+    } else {
+      request->send(404);
+    }
+    return;
+  }
+  request->send(404);
+}
+std::string WebServer::light_json(light::LightState *obj) {
+  return json::build_json([obj](JsonObject &root) {
+    root["id"] = "light-" + obj->get_object_id();
+    root["state"] = obj->remote_values.is_on() ? "ON" : "OFF";
+    obj->dump_json(root);
+  });
+}
+#endif
+
+#ifdef USE_COVER
+void WebServer::on_cover_update(cover::Cover *obj) {
+  if (obj->is_internal())
+    return;
+  this->events_.send(this->cover_json(obj).c_str(), "state");
+}
+void WebServer::handle_cover_request(AsyncWebServerRequest *request, UrlMatch match) {
+  for (cover::Cover *obj : App.get_covers()) {
+    if (obj->is_internal())
+      continue;
+    if (obj->get_object_id() != match.id)
+      continue;
+
+    if (request->method() == HTTP_GET) {
+      std::string data = this->cover_json(obj);
+      request->send(200, "text/json", data.c_str());
+      continue;
+    }
+
+    auto call = obj->make_call();
+    if (match.method == "open") {
+      call.set_command_open();
+    } else if (match.method == "close") {
+      call.set_command_close();
+    } else if (match.method == "stop") {
+      call.set_command_stop();
+    } else if (match.method != "set") {
+      request->send(404);
+      return;
+    }
+
+    auto traits = obj->get_traits();
+    if ((request->hasParam("position") && !traits.get_supports_position()) ||
+        (request->hasParam("tilt") && !traits.get_supports_tilt())) {
+      request->send(409);
+      return;
+    }
+
+    if (request->hasParam("position"))
+      call.set_position(request->getParam("position")->value().toFloat());
+    if (request->hasParam("tilt"))
+      call.set_tilt(request->getParam("tilt")->value().toFloat());
+
+    this->defer([call]() mutable { call.perform(); });
+    request->send(200);
+    return;
+  }
+  request->send(404);
+}
+std::string WebServer::cover_json(cover::Cover *obj) {
+  return json::build_json([obj](JsonObject &root) {
+    root["id"] = "cover-" + obj->get_object_id();
+    root["state"] = obj->is_fully_closed() ? "CLOSED" : "OPEN";
+    root["value"] = obj->position;
+    root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
+
+    if (obj->get_traits().get_supports_tilt())
+      root["tilt"] = obj->tilt;
+  });
+}
+#endif
+
+bool WebServer::canHandle(AsyncWebServerRequest *request) {
+  if (request->url() == "/")
+    return true;
+
+#ifdef WEBSERVER_CSS_INCLUDE
+  if (request->url() == "/0.css")
+    return true;
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+  if (request->url() == "/0.js")
+    return true;
+#endif
+
+  UrlMatch match = match_url(request->url().c_str(), true);
+  if (!match.valid)
+    return false;
+#ifdef USE_SENSOR
+  if (request->method() == HTTP_GET && match.domain == "sensor")
+    return true;
+#endif
+
+#ifdef USE_SWITCH
+  if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "switch")
+    return true;
+#endif
+
+#ifdef USE_BINARY_SENSOR
+  if (request->method() == HTTP_GET && match.domain == "binary_sensor")
+    return true;
+#endif
+
+#ifdef USE_FAN
+  if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "fan")
+    return true;
+#endif
+
+#ifdef USE_LIGHT
+  if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "light")
+    return true;
+#endif
+
+#ifdef USE_TEXT_SENSOR
+  if (request->method() == HTTP_GET && match.domain == "text_sensor")
+    return true;
+#endif
+
+#ifdef USE_COVER
+  if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "cover")
+    return true;
+#endif
+
+  return false;
+}
+void WebServer::handleRequest(AsyncWebServerRequest *request) {
+  if (this->using_auth() && !request->authenticate(this->username_, this->password_)) {
+    return request->requestAuthentication();
+  }
+
+  if (request->url() == "/") {
+    this->handle_index_request(request);
+    return;
+  }
+
+#ifdef WEBSERVER_CSS_INCLUDE
+  if (request->url() == "/0.css") {
+    this->handle_css_request(request);
+    return;
+  }
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+  if (request->url() == "/0.js") {
+    this->handle_js_request(request);
+    return;
+  }
+#endif
+
+  UrlMatch match = match_url(request->url().c_str());
+#ifdef USE_SENSOR
+  if (match.domain == "sensor") {
+    this->handle_sensor_request(request, match);
+    return;
+  }
+#endif
+
+#ifdef USE_SWITCH
+  if (match.domain == "switch") {
+    this->handle_switch_request(request, match);
+    return;
+  }
+#endif
+
+#ifdef USE_BINARY_SENSOR
+  if (match.domain == "binary_sensor") {
+    this->handle_binary_sensor_request(request, match);
+    return;
+  }
+#endif
+
+#ifdef USE_FAN
+  if (match.domain == "fan") {
+    this->handle_fan_request(request, match);
+    return;
+  }
+#endif
+
+#ifdef USE_LIGHT
+  if (match.domain == "light") {
+    this->handle_light_request(request, match);
+    return;
+  }
+#endif
+
+#ifdef USE_TEXT_SENSOR
+  if (match.domain == "text_sensor") {
+    this->handle_text_sensor_request(request, match);
+    return;
+  }
+#endif
+
+#ifdef USE_COVER
+  if (match.domain == "cover") {
+    this->handle_cover_request(request, match);
+    return;
+  }
+#endif
+}
+
+bool WebServer::isRequestHandlerTrivial() { return false; }
+
+}  // namespace web_server
+}  // namespace esphome

+ 176 - 0
livingroom/src/esphome/components/web_server/web_server.h

@@ -0,0 +1,176 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/controller.h"
+#include "esphome/components/web_server_base/web_server_base.h"
+
+#include <vector>
+
+namespace esphome {
+namespace web_server {
+
+/// Internal helper struct that is used to parse incoming URLs
+struct UrlMatch {
+  std::string domain;  ///< The domain of the component, for example "sensor"
+  std::string id;      ///< The id of the device that's being accessed, for example "living_room_fan"
+  std::string method;  ///< The method that's being called, for example "turn_on"
+  bool valid;          ///< Whether this match is valid
+};
+
+/** This class allows users to create a web server with their ESP nodes.
+ *
+ * Behind the scenes it's using AsyncWebServer to set up the server. It exposes 3 things:
+ * an index page under '/' that's used to show a simple web interface (the css/js is hosted
+ * by esphome.io by default), an event source under '/events' that automatically sends
+ * all state updates in real time + the debug log. Lastly, there's an REST API available
+ * under the '/light/...', '/sensor/...', ... URLs. A full documentation for this API
+ * can be found under https://esphome.io/web-api/index.html.
+ */
+class WebServer : public Controller, public Component, public AsyncWebHandler {
+ public:
+  WebServer(web_server_base::WebServerBase *base) : base_(base) {}
+
+  void set_username(const char *username) { username_ = username; }
+
+  void set_password(const char *password) { password_ = password; }
+
+  /** Set the URL to the CSS <link> that's sent to each client. Defaults to
+   * https://esphome.io/_static/webserver-v1.min.css
+   *
+   * @param css_url The url to the web server stylesheet.
+   */
+  void set_css_url(const char *css_url);
+
+  /** Set local path to the script that's embedded in the index page. Defaults to
+   *
+   * @param css_include Local path to web server script.
+   */
+  void set_css_include(const char *css_include);
+
+  /** Set the URL to the script that's embedded in the index page. Defaults to
+   * https://esphome.io/_static/webserver-v1.min.js
+   *
+   * @param js_url The url to the web server script.
+   */
+  void set_js_url(const char *js_url);
+
+  /** Set local path to the script that's embedded in the index page. Defaults to
+   *
+   * @param js_include Local path to web server script.
+   */
+  void set_js_include(const char *js_include);
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  /// Setup the internal web server and register handlers.
+  void setup() override;
+
+  void dump_config() override;
+
+  /// MQTT setup priority.
+  float get_setup_priority() const override;
+
+  /// Handle an index request under '/'.
+  void handle_index_request(AsyncWebServerRequest *request);
+
+#ifdef WEBSERVER_CSS_INCLUDE
+  /// Handle included css request under '/0.css'.
+  void handle_css_request(AsyncWebServerRequest *request);
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+  /// Handle included js request under '/0.js'.
+  void handle_js_request(AsyncWebServerRequest *request);
+#endif
+
+  bool using_auth() { return username_ != nullptr && password_ != nullptr; }
+
+#ifdef USE_SENSOR
+  void on_sensor_update(sensor::Sensor *obj, float state) override;
+  /// Handle a sensor request under '/sensor/<id>'.
+  void handle_sensor_request(AsyncWebServerRequest *request, UrlMatch match);
+
+  /// Dump the sensor state with its value as a JSON string.
+  std::string sensor_json(sensor::Sensor *obj, float value);
+#endif
+
+#ifdef USE_SWITCH
+  void on_switch_update(switch_::Switch *obj, bool state) override;
+
+  /// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
+  void handle_switch_request(AsyncWebServerRequest *request, UrlMatch match);
+
+  /// Dump the switch state with its value as a JSON string.
+  std::string switch_json(switch_::Switch *obj, bool value);
+#endif
+
+#ifdef USE_BINARY_SENSOR
+  void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
+
+  /// Handle a binary sensor request under '/binary_sensor/<id>'.
+  void handle_binary_sensor_request(AsyncWebServerRequest *request, UrlMatch match);
+
+  /// Dump the binary sensor state with its value as a JSON string.
+  std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value);
+#endif
+
+#ifdef USE_FAN
+  void on_fan_update(fan::FanState *obj) override;
+
+  /// Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'.
+  void handle_fan_request(AsyncWebServerRequest *request, UrlMatch match);
+
+  /// Dump the fan state as a JSON string.
+  std::string fan_json(fan::FanState *obj);
+#endif
+
+#ifdef USE_LIGHT
+  void on_light_update(light::LightState *obj) override;
+
+  /// Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'.
+  void handle_light_request(AsyncWebServerRequest *request, UrlMatch match);
+
+  /// Dump the light state as a JSON string.
+  std::string light_json(light::LightState *obj);
+#endif
+
+#ifdef USE_TEXT_SENSOR
+  void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) override;
+
+  /// Handle a text sensor request under '/text_sensor/<id>'.
+  void handle_text_sensor_request(AsyncWebServerRequest *request, UrlMatch match);
+
+  /// Dump the text sensor state with its value as a JSON string.
+  std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value);
+#endif
+
+#ifdef USE_COVER
+  void on_cover_update(cover::Cover *obj) override;
+
+  /// Handle a cover request under '/cover/<id>/<open/close/stop/set>'.
+  void handle_cover_request(AsyncWebServerRequest *request, UrlMatch match);
+
+  /// Dump the cover state as a JSON string.
+  std::string cover_json(cover::Cover *obj);
+#endif
+
+  /// Override the web handler's canHandle method.
+  bool canHandle(AsyncWebServerRequest *request) override;
+  /// Override the web handler's handleRequest method.
+  void handleRequest(AsyncWebServerRequest *request) override;
+  /// This web handle is not trivial.
+  bool isRequestHandlerTrivial() override;
+
+ protected:
+  web_server_base::WebServerBase *base_;
+  AsyncEventSource events_{"/events"};
+  const char *username_{nullptr};
+  const char *password_{nullptr};
+  const char *css_url_{nullptr};
+  const char *css_include_{nullptr};
+  const char *js_url_{nullptr};
+  const char *js_include_{nullptr};
+};
+
+}  // namespace web_server
+}  // namespace esphome

+ 96 - 0
livingroom/src/esphome/components/web_server_base/web_server_base.cpp

@@ -0,0 +1,96 @@
+#include "web_server_base.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include <StreamString.h>
+
+#ifdef ARDUINO_ARCH_ESP32
+#include <Update.h>
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+#include <Updater.h>
+#endif
+
+namespace esphome {
+namespace web_server_base {
+
+static const char *TAG = "web_server_base";
+
+void report_ota_error() {
+  StreamString ss;
+  Update.printError(ss);
+  ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str());
+}
+
+void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
+                                     uint8_t *data, size_t len, bool final) {
+  bool success;
+  if (index == 0) {
+    ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str());
+    this->ota_read_length_ = 0;
+#ifdef ARDUINO_ARCH_ESP8266
+    Update.runAsync(true);
+    success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+    if (Update.isRunning())
+      Update.abort();
+    success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
+#endif
+    if (!success) {
+      report_ota_error();
+      return;
+    }
+  } else if (Update.hasError()) {
+    // don't spam logs with errors if something failed at start
+    return;
+  }
+
+  success = Update.write(data, len) == len;
+  if (!success) {
+    report_ota_error();
+    return;
+  }
+  this->ota_read_length_ += len;
+
+  const uint32_t now = millis();
+  if (now - this->last_ota_progress_ > 1000) {
+    if (request->contentLength() != 0) {
+      float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
+      ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
+    } else {
+      ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
+    }
+    this->last_ota_progress_ = now;
+  }
+
+  if (final) {
+    if (Update.end(true)) {
+      ESP_LOGI(TAG, "OTA update successful!");
+      this->parent_->set_timeout(100, []() { App.safe_reboot(); });
+    } else {
+      report_ota_error();
+    }
+  }
+}
+void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
+  AsyncWebServerResponse *response;
+  if (!Update.hasError()) {
+    response = request->beginResponse(200, "text/plain", "Update Successful!");
+  } else {
+    StreamString ss;
+    ss.print("Update Failed: ");
+    Update.printError(ss);
+    response = request->beginResponse(200, "text/plain", ss);
+  }
+  response->addHeader("Connection", "close");
+  request->send(response);
+}
+
+void WebServerBase::add_ota_handler() { this->add_handler(new OTARequestHandler(this)); }
+float WebServerBase::get_setup_priority() const {
+  // Before WiFi (captive portal)
+  return setup_priority::WIFI + 2.0f;
+}
+
+}  // namespace web_server_base
+}  // namespace esphome

+ 76 - 0
livingroom/src/esphome/components/web_server_base/web_server_base.h

@@ -0,0 +1,76 @@
+#pragma once
+
+#include "esphome/core/component.h"
+
+#include <ESPAsyncWebServer.h>
+
+namespace esphome {
+namespace web_server_base {
+
+class WebServerBase : public Component {
+ public:
+  void init() {
+    if (this->initialized_) {
+      this->initialized_++;
+      return;
+    }
+    this->server_ = new AsyncWebServer(this->port_);
+    this->server_->begin();
+
+    for (auto *handler : this->handlers_)
+      this->server_->addHandler(handler);
+
+    this->initialized_++;
+  }
+  void deinit() {
+    this->initialized_--;
+    if (this->initialized_ == 0) {
+      delete this->server_;
+      this->server_ = nullptr;
+    }
+  }
+  AsyncWebServer *get_server() const { return server_; }
+  float get_setup_priority() const override;
+
+  void add_handler(AsyncWebHandler *handler) {
+    // remove all handlers
+
+    this->handlers_.push_back(handler);
+    if (this->server_ != nullptr)
+      this->server_->addHandler(handler);
+  }
+
+  void add_ota_handler();
+
+  void set_port(uint16_t port) { port_ = port; }
+  uint16_t get_port() const { return port_; }
+
+ protected:
+  friend class OTARequestHandler;
+
+  int initialized_{0};
+  uint16_t port_{80};
+  AsyncWebServer *server_{nullptr};
+  std::vector<AsyncWebHandler *> handlers_;
+};
+
+class OTARequestHandler : public AsyncWebHandler {
+ public:
+  OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
+  void handleRequest(AsyncWebServerRequest *request) override;
+  void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
+                    bool final) override;
+  bool canHandle(AsyncWebServerRequest *request) override {
+    return request->url() == "/update" && request->method() == HTTP_POST;
+  }
+
+  bool isRequestHandlerTrivial() override { return false; }
+
+ protected:
+  uint32_t last_ota_progress_{0};
+  uint32_t ota_read_length_{0};
+  WebServerBase *parent_;
+};
+
+}  // namespace web_server_base
+}  // namespace esphome

+ 639 - 0
livingroom/src/esphome/components/wifi/wifi_component.cpp

@@ -0,0 +1,639 @@
+#include "wifi_component.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+#include <esp_wifi.h>
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+#include <user_interface.h>
+#endif
+
+#include <utility>
+#include <algorithm>
+#include "lwip/err.h"
+#include "lwip/dns.h"
+
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+#include "esphome/core/esphal.h"
+#include "esphome/core/util.h"
+#include "esphome/core/application.h"
+
+#ifdef USE_CAPTIVE_PORTAL
+#include "esphome/components/captive_portal/captive_portal.h"
+#endif
+
+namespace esphome {
+namespace wifi {
+
+static const char *TAG = "wifi";
+
+float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
+
+void WiFiComponent::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up WiFi...");
+  this->last_connected_ = millis();
+  this->wifi_pre_setup_();
+
+  if (this->has_sta()) {
+    this->wifi_sta_pre_setup_();
+    if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
+      ESP_LOGV(TAG, "Setting Output Power Option failed!");
+    }
+
+    if (!this->wifi_apply_power_save_()) {
+      ESP_LOGV(TAG, "Setting Power Save Option failed!");
+    }
+
+    if (this->fast_connect_) {
+      this->selected_ap_ = this->sta_[0];
+      this->start_connecting(this->selected_ap_, false);
+    } else {
+      this->start_scanning();
+    }
+  } else if (this->has_ap()) {
+    this->setup_ap_config_();
+    if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
+      ESP_LOGV(TAG, "Setting Output Power Option failed!");
+    }
+#ifdef USE_CAPTIVE_PORTAL
+    if (captive_portal::global_captive_portal != nullptr)
+      captive_portal::global_captive_portal->start();
+#endif
+  }
+
+  this->wifi_apply_hostname_();
+#ifdef ARDUINO_ARCH_ESP32
+  network_setup_mdns();
+#endif
+}
+
+void WiFiComponent::loop() {
+  const uint32_t now = millis();
+
+  if (this->has_sta()) {
+    switch (this->state_) {
+      case WIFI_COMPONENT_STATE_COOLDOWN: {
+        this->status_set_warning();
+        if (millis() - this->action_started_ > 5000) {
+          if (this->fast_connect_) {
+            this->start_connecting(this->sta_[0], false);
+          } else {
+            this->start_scanning();
+          }
+        }
+        break;
+      }
+      case WIFI_COMPONENT_STATE_STA_SCANNING: {
+        this->status_set_warning();
+        this->check_scanning_finished();
+        break;
+      }
+      case WIFI_COMPONENT_STATE_STA_CONNECTING:
+      case WIFI_COMPONENT_STATE_STA_CONNECTING_2: {
+        this->status_set_warning();
+        this->check_connecting_finished();
+        break;
+      }
+
+      case WIFI_COMPONENT_STATE_STA_CONNECTED: {
+        if (!this->is_connected()) {
+          ESP_LOGW(TAG, "WiFi Connection lost... Reconnecting...");
+          this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
+          this->retry_connect();
+        } else {
+          this->status_clear_warning();
+          this->last_connected_ = now;
+        }
+        break;
+      }
+      case WIFI_COMPONENT_STATE_OFF:
+      case WIFI_COMPONENT_STATE_AP:
+        break;
+    }
+
+    if (this->has_ap() && !this->ap_setup_) {
+      if (now - this->last_connected_ > this->ap_timeout_) {
+        ESP_LOGI(TAG, "Starting fallback AP!");
+        this->setup_ap_config_();
+#ifdef USE_CAPTIVE_PORTAL
+        if (captive_portal::global_captive_portal != nullptr)
+          captive_portal::global_captive_portal->start();
+#endif
+      }
+    }
+
+    if (!this->has_ap() && this->reboot_timeout_ != 0) {
+      if (now - this->last_connected_ > this->reboot_timeout_) {
+        ESP_LOGE(TAG, "Can't connect to WiFi, rebooting...");
+        App.reboot();
+      }
+    }
+  }
+
+  network_tick_mdns();
+}
+
+WiFiComponent::WiFiComponent() { global_wifi_component = this; }
+
+bool WiFiComponent::has_ap() const { return !this->ap_.get_ssid().empty(); }
+bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
+void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; }
+IPAddress WiFiComponent::get_ip_address() {
+  if (this->has_sta())
+    return this->wifi_sta_ip_();
+  if (this->has_ap())
+    return this->wifi_soft_ap_ip();
+  return {};
+}
+std::string WiFiComponent::get_use_address() const {
+  if (this->use_address_.empty()) {
+    return App.get_name() + ".local";
+  }
+  return this->use_address_;
+}
+void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
+void WiFiComponent::setup_ap_config_() {
+  this->wifi_mode_({}, true);
+
+  if (this->ap_setup_)
+    return;
+
+  ESP_LOGCONFIG(TAG, "Setting up AP...");
+
+  ESP_LOGCONFIG(TAG, "  AP SSID: '%s'", this->ap_.get_ssid().c_str());
+  ESP_LOGCONFIG(TAG, "  AP Password: '%s'", this->ap_.get_password().c_str());
+  if (this->ap_.get_manual_ip().has_value()) {
+    auto manual = *this->ap_.get_manual_ip();
+    ESP_LOGCONFIG(TAG, "  AP Static IP: '%s'", manual.static_ip.toString().c_str());
+    ESP_LOGCONFIG(TAG, "  AP Gateway: '%s'", manual.gateway.toString().c_str());
+    ESP_LOGCONFIG(TAG, "  AP Subnet: '%s'", manual.subnet.toString().c_str());
+  }
+
+  this->ap_setup_ = this->wifi_start_ap_(this->ap_);
+  ESP_LOGCONFIG(TAG, "  IP Address: %s", this->wifi_soft_ap_ip().toString().c_str());
+#ifdef ARDUINO_ARCH_ESP8266
+  network_setup_mdns(this->wifi_soft_ap_ip(), 1);
+#endif
+
+  if (!this->has_sta()) {
+    this->state_ = WIFI_COMPONENT_STATE_AP;
+  }
+}
+
+float WiFiComponent::get_loop_priority() const {
+  return 10.0f;  // before other loop components
+}
+void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; }
+void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
+void WiFiComponent::set_sta(const WiFiAP &ap) {
+  this->sta_.clear();
+  this->add_sta(ap);
+}
+
+void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
+  ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str());
+#ifdef ESPHOME_LOG_HAS_VERBOSE
+  ESP_LOGV(TAG, "Connection Params:");
+  ESP_LOGV(TAG, "  SSID: '%s'", ap.get_ssid().c_str());
+  if (ap.get_bssid().has_value()) {
+    bssid_t b = *ap.get_bssid();
+    ESP_LOGV(TAG, "  BSSID: %02X:%02X:%02X:%02X:%02X:%02X", b[0], b[1], b[2], b[3], b[4], b[5]);
+  } else {
+    ESP_LOGV(TAG, "  BSSID: Not Set");
+  }
+
+#ifdef ESPHOME_WIFI_WPA2_EAP
+  if (ap.get_eap().has_value()) {
+    ESP_LOGV(TAG, "  WPA2 Enterprise authentication configured:");
+    EAPAuth eap_config = ap.get_eap().value();
+    ESP_LOGV(TAG, "    Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
+    ESP_LOGV(TAG, "    Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
+    ESP_LOGV(TAG, "    Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
+    bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
+    bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);
+    bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key);
+    ESP_LOGV(TAG, "    CA Cert:     %s", ca_cert_present ? "present" : "not present");
+    ESP_LOGV(TAG, "    Client Cert: %s", client_cert_present ? "present" : "not present");
+    ESP_LOGV(TAG, "    Client Key:  %s", client_key_present ? "present" : "not present");
+  } else {
+#endif
+    ESP_LOGV(TAG, "  Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
+#ifdef ESPHOME_WIFI_WPA2_EAP
+  }
+#endif
+  if (ap.get_channel().has_value()) {
+    ESP_LOGV(TAG, "  Channel: %u", *ap.get_channel());
+  } else {
+    ESP_LOGV(TAG, "  Channel: Not Set");
+  }
+  if (ap.get_manual_ip().has_value()) {
+    ManualIP m = *ap.get_manual_ip();
+    ESP_LOGV(TAG, "  Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.toString().c_str(),
+             m.gateway.toString().c_str(), m.subnet.toString().c_str(), m.dns1.toString().c_str(),
+             m.dns2.toString().c_str());
+  } else {
+    ESP_LOGV(TAG, "  Using DHCP IP");
+  }
+  ESP_LOGV(TAG, "  Hidden: %s", YESNO(ap.get_hidden()));
+#endif
+
+  if (!this->wifi_sta_connect_(ap)) {
+    ESP_LOGE(TAG, "wifi_sta_connect_ failed!");
+    this->retry_connect();
+    return;
+  }
+
+  if (!two) {
+    this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
+  } else {
+    this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
+  }
+  this->action_started_ = millis();
+}
+
+void print_signal_bars(int8_t rssi, char *buf) {
+  // LOWER ONE QUARTER BLOCK
+  // Unicode: U+2582, UTF-8: E2 96 82
+  // LOWER HALF BLOCK
+  // Unicode: U+2584, UTF-8: E2 96 84
+  // LOWER THREE QUARTERS BLOCK
+  // Unicode: U+2586, UTF-8: E2 96 86
+  // FULL BLOCK
+  // Unicode: U+2588, UTF-8: E2 96 88
+  if (rssi >= -50) {
+    sprintf(buf, "\033[0;32m"  // green
+                 "\xe2\x96\x82"
+                 "\xe2\x96\x84"
+                 "\xe2\x96\x86"
+                 "\xe2\x96\x88"
+                 "\033[0m");
+  } else if (rssi >= -65) {
+    sprintf(buf, "\033[0;33m"  // yellow
+                 "\xe2\x96\x82"
+                 "\xe2\x96\x84"
+                 "\xe2\x96\x86"
+                 "\033[0;37m"
+                 "\xe2\x96\x88"
+                 "\033[0m");
+  } else if (rssi >= -85) {
+    sprintf(buf, "\033[0;33m"  // yellow
+                 "\xe2\x96\x82"
+                 "\xe2\x96\x84"
+                 "\033[0;37m"
+                 "\xe2\x96\x86"
+                 "\xe2\x96\x88"
+                 "\033[0m");
+  } else {
+    sprintf(buf, "\033[0;31m"  // red
+                 "\xe2\x96\x82"
+                 "\033[0;37m"
+                 "\xe2\x96\x84"
+                 "\xe2\x96\x86"
+                 "\xe2\x96\x88"
+                 "\033[0m");
+  }
+}
+
+void WiFiComponent::print_connect_params_() {
+  uint8_t bssid[6] = {};
+  uint8_t *raw_bssid = WiFi.BSSID();
+  if (raw_bssid != nullptr)
+    memcpy(bssid, raw_bssid, sizeof(bssid));
+
+  ESP_LOGCONFIG(TAG, "  SSID: " LOG_SECRET("'%s'"), WiFi.SSID().c_str());
+  ESP_LOGCONFIG(TAG, "  IP Address: %s", WiFi.localIP().toString().c_str());
+  ESP_LOGCONFIG(TAG, "  BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3],
+                bssid[4], bssid[5]);
+  ESP_LOGCONFIG(TAG, "  Hostname: '%s'", App.get_name().c_str());
+  char signal_bars[50];
+  int8_t rssi = WiFi.RSSI();
+  print_signal_bars(rssi, signal_bars);
+  ESP_LOGCONFIG(TAG, "  Signal strength: %d dB %s", rssi, signal_bars);
+  if (this->selected_ap_.get_bssid().has_value()) {
+    ESP_LOGV(TAG, "  Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid()));
+  }
+  ESP_LOGCONFIG(TAG, "  Channel: %d", WiFi.channel());
+  ESP_LOGCONFIG(TAG, "  Subnet: %s", WiFi.subnetMask().toString().c_str());
+  ESP_LOGCONFIG(TAG, "  Gateway: %s", WiFi.gatewayIP().toString().c_str());
+  ESP_LOGCONFIG(TAG, "  DNS1: %s", WiFi.dnsIP(0).toString().c_str());
+  ESP_LOGCONFIG(TAG, "  DNS2: %s", WiFi.dnsIP(1).toString().c_str());
+}
+
+void WiFiComponent::start_scanning() {
+  this->action_started_ = millis();
+  ESP_LOGD(TAG, "Starting scan...");
+  this->wifi_scan_start_();
+  this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING;
+}
+
+void WiFiComponent::check_scanning_finished() {
+  if (!this->scan_done_) {
+    if (millis() - this->action_started_ > 30000) {
+      ESP_LOGE(TAG, "Scan timeout!");
+      this->retry_connect();
+    }
+    return;
+  }
+  this->scan_done_ = false;
+
+  ESP_LOGD(TAG, "Found networks:");
+  if (this->scan_result_.empty()) {
+    ESP_LOGD(TAG, "  No network found!");
+    this->retry_connect();
+    return;
+  }
+
+  for (auto &res : this->scan_result_) {
+    for (auto &ap : this->sta_) {
+      if (res.matches(ap)) {
+        res.set_matches(true);
+        if (!this->has_sta_priority(res.get_bssid())) {
+          this->set_sta_priority(res.get_bssid(), ap.get_priority());
+        }
+        res.set_priority(this->get_sta_priority(res.get_bssid()));
+        break;
+      }
+    }
+  }
+
+  std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(),
+                   [](const WiFiScanResult &a, const WiFiScanResult &b) {
+                     // return true if a is better than b
+                     if (a.get_matches() && !b.get_matches())
+                       return true;
+                     if (!a.get_matches() && b.get_matches())
+                       return false;
+
+                     if (a.get_matches() && b.get_matches()) {
+                       // if both match, check priority
+                       if (a.get_priority() != b.get_priority())
+                         return a.get_priority() > b.get_priority();
+                     }
+
+                     return a.get_rssi() > b.get_rssi();
+                   });
+
+  for (auto &res : this->scan_result_) {
+    char bssid_s[18];
+    auto bssid = res.get_bssid();
+    sprintf(bssid_s, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
+    char signal_bars[50];
+    print_signal_bars(res.get_rssi(), signal_bars);
+
+    if (res.get_matches()) {
+      ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
+               res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, signal_bars);
+      ESP_LOGD(TAG, "    Channel: %u", res.get_channel());
+      ESP_LOGD(TAG, "    RSSI: %d dB", res.get_rssi());
+    } else {
+      ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, signal_bars);
+    }
+  }
+
+  if (!this->scan_result_[0].get_matches()) {
+    ESP_LOGW(TAG, "No matching network found!");
+    this->retry_connect();
+    return;
+  }
+
+  WiFiAP connect_params;
+  WiFiScanResult scan_res = this->scan_result_[0];
+  for (auto &config : this->sta_) {
+    // search for matching STA config, at least one will match (from checks before)
+    if (!scan_res.matches(config)) {
+      continue;
+    }
+
+    if (config.get_hidden()) {
+      // selected network is hidden, we use the data from the config
+      connect_params.set_hidden(true);
+      connect_params.set_ssid(config.get_ssid());
+      // don't set BSSID and channel, there might be multiple hidden networks
+      // but we can't know which one is the correct one. Rely on probe-req with just SSID.
+    } else {
+      // selected network is visible, we use the data from the scan
+      // limit the connect params to only connect to exactly this network
+      // (network selection is done during scan phase).
+      connect_params.set_hidden(false);
+      connect_params.set_ssid(scan_res.get_ssid());
+      connect_params.set_channel(scan_res.get_channel());
+      connect_params.set_bssid(scan_res.get_bssid());
+    }
+    // copy manual IP (if set)
+    connect_params.set_manual_ip(config.get_manual_ip());
+
+#ifdef ESPHOME_WIFI_WPA2_EAP
+    // copy EAP parameters (if set)
+    connect_params.set_eap(config.get_eap());
+#endif
+
+    // copy password (if set)
+    connect_params.set_password(config.get_password());
+
+    break;
+  }
+
+  yield();
+
+  this->selected_ap_ = connect_params;
+  this->start_connecting(connect_params, false);
+}
+
+void WiFiComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "WiFi:");
+  this->print_connect_params_();
+}
+
+void WiFiComponent::check_connecting_finished() {
+  wl_status_t status = this->wifi_sta_status_();
+
+  if (status == WL_CONNECTED) {
+    if (WiFi.SSID().equals("")) {
+      ESP_LOGW(TAG, "Incomplete connection.");
+      this->retry_connect();
+      return;
+    }
+
+    ESP_LOGI(TAG, "WiFi Connected!");
+    this->print_connect_params_();
+
+    if (this->has_ap()) {
+#ifdef USE_CAPTIVE_PORTAL
+      if (this->is_captive_portal_active_()) {
+        captive_portal::global_captive_portal->end();
+      }
+#endif
+      ESP_LOGD(TAG, "Disabling AP...");
+      this->wifi_mode_({}, false);
+    }
+#ifdef ARDUINO_ARCH_ESP8266
+    network_setup_mdns(this->wifi_sta_ip_(), 0);
+#endif
+    this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
+    this->num_retried_ = 0;
+    return;
+  }
+
+  uint32_t now = millis();
+  if (now - this->action_started_ > 30000) {
+    ESP_LOGW(TAG, "Timeout while connecting to WiFi.");
+    this->retry_connect();
+    return;
+  }
+
+  if (this->error_from_callback_) {
+    ESP_LOGW(TAG, "Error while connecting to network.");
+    this->retry_connect();
+    return;
+  }
+
+  if (status == WL_IDLE_STATUS || status == WL_DISCONNECTED || status == WL_CONNECTION_LOST) {
+    // WL_DISCONNECTED is set while not connected yet.
+    // WL_IDLE_STATUS is set while we're waiting for the IP address.
+    // WL_CONNECTION_LOST happens on the ESP32
+    return;
+  }
+
+  if (status == WL_NO_SSID_AVAIL) {
+    ESP_LOGW(TAG, "WiFi network can not be found anymore.");
+    this->retry_connect();
+    return;
+  }
+
+  if (status == WL_CONNECT_FAILED) {
+    ESP_LOGW(TAG, "Connecting to WiFi network failed. Are the credentials wrong?");
+    this->retry_connect();
+    return;
+  }
+
+  ESP_LOGW(TAG, "WiFi Unknown connection status %d", status);
+}
+
+void WiFiComponent::retry_connect() {
+  if (this->selected_ap_.get_bssid()) {
+    auto bssid = *this->selected_ap_.get_bssid();
+    float priority = this->get_sta_priority(bssid);
+    this->set_sta_priority(bssid, priority - 1.0f);
+  }
+
+  delay(10);
+  if (!this->is_captive_portal_active_() && (this->num_retried_ > 5 || this->error_from_callback_)) {
+    // If retry failed for more than 5 times, let's restart STA
+    ESP_LOGW(TAG, "Restarting WiFi adapter...");
+    this->wifi_mode_(false, {});
+    delay(100);  // NOLINT
+    this->num_retried_ = 0;
+  } else {
+    this->num_retried_++;
+  }
+  this->error_from_callback_ = false;
+  if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) {
+    yield();
+    this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
+    this->start_connecting(this->selected_ap_, true);
+    return;
+  }
+
+  this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
+  this->action_started_ = millis();
+}
+
+bool WiFiComponent::can_proceed() {
+  if (this->has_ap() && !this->has_sta()) {
+    return true;
+  }
+  return this->is_connected();
+}
+void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
+bool WiFiComponent::is_connected() {
+  return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_status_() == WL_CONNECTED &&
+         !this->error_from_callback_;
+}
+void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; }
+
+std::string WiFiComponent::format_mac_addr(const uint8_t *mac) {
+  char buf[20];
+  sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+  return buf;
+}
+bool WiFiComponent::is_captive_portal_active_() {
+#ifdef USE_CAPTIVE_PORTAL
+  return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active();
+#else
+  return false;
+#endif
+}
+
+void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
+void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
+void WiFiAP::set_bssid(optional<bssid_t> bssid) { this->bssid_ = bssid; }
+void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
+#ifdef ESPHOME_WIFI_WPA2_EAP
+void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = eap_auth; }
+#endif
+void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
+void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
+void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
+const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
+const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
+const std::string &WiFiAP::get_password() const { return this->password_; }
+#ifdef ESPHOME_WIFI_WPA2_EAP
+const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
+#endif
+const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
+const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
+bool WiFiAP::get_hidden() const { return this->hidden_; }
+
+WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const std::string &ssid, uint8_t channel, int8_t rssi,
+                               bool with_auth, bool is_hidden)
+    : bssid_(bssid), ssid_(ssid), channel_(channel), rssi_(rssi), with_auth_(with_auth), is_hidden_(is_hidden) {}
+bool WiFiScanResult::matches(const WiFiAP &config) {
+  if (config.get_hidden()) {
+    // User configured a hidden network, only match actually hidden networks
+    // don't match SSID
+    if (!this->is_hidden_)
+      return false;
+  } else if (!config.get_ssid().empty()) {
+    // check if SSID matches
+    if (config.get_ssid() != this->ssid_)
+      return false;
+  } else {
+    // network is configured without SSID - match other settings
+  }
+  // If BSSID configured, only match for correct BSSIDs
+  if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_)
+    return false;
+
+#ifdef ESPHOME_WIFI_WPA2_EAP
+  // BSSID requires auth but no PSK or EAP credentials given
+  if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value()))
+    return false;
+
+  // BSSID does not require auth, but PSK or EAP credentials given
+  if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value()))
+    return false;
+#else
+  // If PSK given, only match for networks with auth (and vice versa)
+  if (config.get_password().empty() == this->with_auth_)
+    return false;
+#endif
+
+  // If channel configured, only match networks on that channel.
+  if (config.get_channel().has_value() && *config.get_channel() != this->channel_) {
+    return false;
+  }
+  return true;
+}
+bool WiFiScanResult::get_matches() const { return this->matches_; }
+void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
+const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
+const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
+uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
+int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
+bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
+bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
+
+WiFiComponent *global_wifi_component;
+
+}  // namespace wifi
+}  // namespace esphome

+ 301 - 0
livingroom/src/esphome/components/wifi/wifi_component.h

@@ -0,0 +1,301 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/automation.h"
+#include "esphome/core/helpers.h"
+#include <string>
+#include <IPAddress.h>
+
+#ifdef ARDUINO_ARCH_ESP32
+#include <esp_wifi.h>
+#include <WiFiType.h>
+#include <WiFi.h>
+#endif
+
+#ifdef ARDUINO_ARCH_ESP8266
+#include <ESP8266WiFiType.h>
+#include <ESP8266WiFi.h>
+
+#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
+extern "C" {
+#include <user_interface.h>
+};
+#endif
+#endif
+
+namespace esphome {
+namespace wifi {
+
+enum WiFiComponentState {
+  /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */
+  WIFI_COMPONENT_STATE_OFF = 0,
+  /** WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of time. */
+  WIFI_COMPONENT_STATE_COOLDOWN,
+  /** WiFi is in STA-only mode and currently scanning for APs. */
+  WIFI_COMPONENT_STATE_STA_SCANNING,
+  /** WiFi is in STA(+AP) mode and currently connecting to an AP. */
+  WIFI_COMPONENT_STATE_STA_CONNECTING,
+  /** WiFi is in STA(+AP) mode and currently connecting to an AP a second time.
+   *
+   * This is required because for some reason ESPs don't like to connect to WiFi APs directly after
+   * a scan.
+   * */
+  WIFI_COMPONENT_STATE_STA_CONNECTING_2,
+  /** WiFi is in STA(+AP) mode and successfully connected. */
+  WIFI_COMPONENT_STATE_STA_CONNECTED,
+  /** WiFi is in AP-only mode and internal AP is already enabled. */
+  WIFI_COMPONENT_STATE_AP,
+};
+
+/// Struct for setting static IPs in WiFiComponent.
+struct ManualIP {
+  IPAddress static_ip;
+  IPAddress gateway;
+  IPAddress subnet;
+  IPAddress dns1;  ///< The first DNS server. 0.0.0.0 for default.
+  IPAddress dns2;  ///< The second DNS server. 0.0.0.0 for default.
+};
+
+#ifdef ESPHOME_WIFI_WPA2_EAP
+struct EAPAuth {
+  std::string identity;  // required for all auth types
+  std::string username;
+  std::string password;
+  const char *ca_cert;  // optionally verify authentication server
+  // used for EAP-TLS
+  const char *client_cert;
+  const char *client_key;
+};
+#endif  // ESPHOME_WIFI_WPA2_EAP
+
+using bssid_t = std::array<uint8_t, 6>;
+
+class WiFiAP {
+ public:
+  void set_ssid(const std::string &ssid);
+  void set_bssid(bssid_t bssid);
+  void set_bssid(optional<bssid_t> bssid);
+  void set_password(const std::string &password);
+#ifdef ESPHOME_WIFI_WPA2_EAP
+  void set_eap(optional<EAPAuth> eap_auth);
+#endif  // ESPHOME_WIFI_WPA2_EAP
+  void set_channel(optional<uint8_t> channel);
+  void set_priority(float priority) { priority_ = priority; }
+  void set_manual_ip(optional<ManualIP> manual_ip);
+  void set_hidden(bool hidden);
+  const std::string &get_ssid() const;
+  const optional<bssid_t> &get_bssid() const;
+  const std::string &get_password() const;
+#ifdef ESPHOME_WIFI_WPA2_EAP
+  const optional<EAPAuth> &get_eap() const;
+#endif  // ESPHOME_WIFI_WPA2_EAP
+  const optional<uint8_t> &get_channel() const;
+  float get_priority() const { return priority_; }
+  const optional<ManualIP> &get_manual_ip() const;
+  bool get_hidden() const;
+
+ protected:
+  std::string ssid_;
+  optional<bssid_t> bssid_;
+  std::string password_;
+#ifdef ESPHOME_WIFI_WPA2_EAP
+  optional<EAPAuth> eap_;
+#endif  // ESPHOME_WIFI_WPA2_EAP
+  optional<uint8_t> channel_;
+  float priority_{0};
+  optional<ManualIP> manual_ip_;
+  bool hidden_{false};
+};
+
+class WiFiScanResult {
+ public:
+  WiFiScanResult(const bssid_t &bssid, const std::string &ssid, uint8_t channel, int8_t rssi, bool with_auth,
+                 bool is_hidden);
+
+  bool matches(const WiFiAP &config);
+
+  bool get_matches() const;
+  void set_matches(bool matches);
+  const bssid_t &get_bssid() const;
+  const std::string &get_ssid() const;
+  uint8_t get_channel() const;
+  int8_t get_rssi() const;
+  bool get_with_auth() const;
+  bool get_is_hidden() const;
+  float get_priority() const { return priority_; }
+  void set_priority(float priority) { priority_ = priority; }
+
+ protected:
+  bool matches_{false};
+  bssid_t bssid_;
+  std::string ssid_;
+  uint8_t channel_;
+  int8_t rssi_;
+  bool with_auth_;
+  bool is_hidden_;
+  float priority_{0.0f};
+};
+
+struct WiFiSTAPriority {
+  bssid_t bssid;
+  float priority;
+};
+
+enum WiFiPowerSaveMode {
+  WIFI_POWER_SAVE_NONE = 0,
+  WIFI_POWER_SAVE_LIGHT,
+  WIFI_POWER_SAVE_HIGH,
+};
+
+/// This component is responsible for managing the ESP WiFi interface.
+class WiFiComponent : public Component {
+ public:
+  /// Construct a WiFiComponent.
+  WiFiComponent();
+
+  void set_sta(const WiFiAP &ap);
+  void add_sta(const WiFiAP &ap);
+
+  /** Setup an Access Point that should be created if no connection to a station can be made.
+   *
+   * This can also be used without set_sta(). Then the AP will always be active.
+   *
+   * If both STA and AP are defined, then both will be enabled at startup, but if a connection to a station
+   * can be made, the AP will be turned off again.
+   */
+  void set_ap(const WiFiAP &ap);
+
+  void start_scanning();
+  void check_scanning_finished();
+  void start_connecting(const WiFiAP &ap, bool two);
+  void set_fast_connect(bool fast_connect);
+  void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; }
+
+  void check_connecting_finished();
+
+  void retry_connect();
+
+  bool can_proceed() override;
+
+  void set_reboot_timeout(uint32_t reboot_timeout);
+
+  bool is_connected();
+
+  void set_power_save_mode(WiFiPowerSaveMode power_save);
+  void set_output_power(float output_power) { output_power_ = output_power; }
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  /// Setup WiFi interface.
+  void setup() override;
+  void dump_config() override;
+  /// WIFI setup_priority.
+  float get_setup_priority() const override;
+  float get_loop_priority() const override;
+
+  /// Reconnect WiFi if required.
+  void loop() override;
+
+  bool has_sta() const;
+  bool has_ap() const;
+
+  IPAddress get_ip_address();
+  std::string get_use_address() const;
+  void set_use_address(const std::string &use_address);
+
+  const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
+
+  IPAddress wifi_soft_ap_ip();
+
+  bool has_sta_priority(const bssid_t &bssid) {
+    for (auto &it : this->sta_priorities_)
+      if (it.bssid == bssid)
+        return true;
+    return false;
+  }
+  float get_sta_priority(const bssid_t bssid) {
+    for (auto &it : this->sta_priorities_)
+      if (it.bssid == bssid)
+        return it.priority;
+    return 0.0f;
+  }
+  void set_sta_priority(const bssid_t bssid, float priority) {
+    for (auto &it : this->sta_priorities_)
+      if (it.bssid == bssid) {
+        it.priority = priority;
+        return;
+      }
+    this->sta_priorities_.push_back(WiFiSTAPriority{
+        .bssid = bssid,
+        .priority = priority,
+    });
+  }
+
+ protected:
+  static std::string format_mac_addr(const uint8_t mac[6]);
+  void setup_ap_config_();
+  void print_connect_params_();
+
+  bool wifi_mode_(optional<bool> sta, optional<bool> ap);
+  bool wifi_sta_pre_setup_();
+  bool wifi_apply_output_power_(float output_power);
+  bool wifi_apply_power_save_();
+  bool wifi_sta_ip_config_(optional<ManualIP> manual_ip);
+  IPAddress wifi_sta_ip_();
+  bool wifi_apply_hostname_();
+  bool wifi_sta_connect_(WiFiAP ap);
+  void wifi_pre_setup_();
+  wl_status_t wifi_sta_status_();
+  bool wifi_scan_start_();
+  bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
+  bool wifi_start_ap_(const WiFiAP &ap);
+  bool wifi_disconnect_();
+
+  bool is_captive_portal_active_();
+
+#ifdef ARDUINO_ARCH_ESP8266
+  static void wifi_event_callback(System_Event_t *event);
+  void wifi_scan_done_callback_(void *arg, STATUS status);
+  static void s_wifi_scan_done_callback(void *arg, STATUS status);
+#endif
+
+#ifdef ARDUINO_ARCH_ESP32
+  void wifi_event_callback_(system_event_id_t event, system_event_info_t info);
+  void wifi_scan_done_callback_();
+#endif
+
+  std::string use_address_;
+  std::vector<WiFiAP> sta_;
+  std::vector<WiFiSTAPriority> sta_priorities_;
+  WiFiAP selected_ap_;
+  bool fast_connect_{false};
+
+  WiFiAP ap_;
+  WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
+  uint32_t action_started_;
+  uint8_t num_retried_{0};
+  uint32_t last_connected_{0};
+  uint32_t reboot_timeout_{};
+  uint32_t ap_timeout_{};
+  WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
+  bool error_from_callback_{false};
+  std::vector<WiFiScanResult> scan_result_;
+  bool scan_done_{false};
+  bool ap_setup_{false};
+  optional<float> output_power_;
+};
+
+extern WiFiComponent *global_wifi_component;
+
+template<typename... Ts> class WiFiConnectedCondition : public Condition<Ts...> {
+ public:
+  bool check(Ts... x) override;
+};
+
+template<typename... Ts> bool WiFiConnectedCondition<Ts...>::check(Ts... x) {
+  return global_wifi_component->is_connected();
+}
+
+}  // namespace wifi
+}  // namespace esphome

+ 612 - 0
livingroom/src/esphome/components/wifi/wifi_component_esp32.cpp

@@ -0,0 +1,612 @@
+#include "wifi_component.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <esp_wifi.h>
+
+#include <utility>
+#include <algorithm>
+#ifdef ESPHOME_WIFI_WPA2_EAP
+#include <esp_wpa2.h>
+#endif
+#include "lwip/err.h"
+#include "lwip/dns.h"
+
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+#include "esphome/core/esphal.h"
+#include "esphome/core/application.h"
+#include "esphome/core/util.h"
+
+namespace esphome {
+namespace wifi {
+
+static const char *TAG = "wifi_esp32";
+
+bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
+  uint8_t current_mode = WiFi.getMode();
+  bool current_sta = current_mode & 0b01;
+  bool current_ap = current_mode & 0b10;
+  bool sta_ = sta.value_or(current_sta);
+  bool ap_ = ap.value_or(current_ap);
+  if (current_sta == sta_ && current_ap == ap_)
+    return true;
+
+  if (sta_ && !current_sta) {
+    ESP_LOGV(TAG, "Enabling STA.");
+  } else if (!sta_ && current_sta) {
+    ESP_LOGV(TAG, "Disabling STA.");
+  }
+  if (ap_ && !current_ap) {
+    ESP_LOGV(TAG, "Enabling AP.");
+  } else if (!ap_ && current_ap) {
+    ESP_LOGV(TAG, "Disabling AP.");
+  }
+
+  uint8_t mode = 0;
+  if (sta_)
+    mode |= 0b01;
+  if (ap_)
+    mode |= 0b10;
+  bool ret = WiFi.mode(static_cast<wifi_mode_t>(mode));
+
+  if (!ret) {
+    ESP_LOGW(TAG, "Setting WiFi mode failed!");
+  }
+
+  return ret;
+}
+bool WiFiComponent::wifi_apply_output_power_(float output_power) {
+  int8_t val = static_cast<int8_t>(output_power * 4);
+  return esp_wifi_set_max_tx_power(val) == ESP_OK;
+}
+bool WiFiComponent::wifi_sta_pre_setup_() {
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  WiFi.setAutoReconnect(false);
+  delay(10);
+  return true;
+}
+bool WiFiComponent::wifi_apply_power_save_() {
+  wifi_ps_type_t power_save;
+  switch (this->power_save_) {
+    case WIFI_POWER_SAVE_LIGHT:
+      power_save = WIFI_PS_MIN_MODEM;
+      break;
+    case WIFI_POWER_SAVE_HIGH:
+      power_save = WIFI_PS_MAX_MODEM;
+      break;
+    case WIFI_POWER_SAVE_NONE:
+    default:
+      power_save = WIFI_PS_NONE;
+      break;
+  }
+  return esp_wifi_set_ps(power_save) == ESP_OK;
+}
+bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
+  // enable STA
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  tcpip_adapter_dhcp_status_t dhcp_status;
+  tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status);
+  if (!manual_ip.has_value()) {
+    // Use DHCP client
+    if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) {
+      esp_err_t err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA);
+      if (err != ESP_OK) {
+        ESP_LOGV(TAG, "Starting DHCP client failed! %d", err);
+      }
+      return err == ESP_OK;
+    }
+    return true;
+  }
+
+  tcpip_adapter_ip_info_t info;
+  memset(&info, 0, sizeof(info));
+  info.ip.addr = static_cast<uint32_t>(manual_ip->static_ip);
+  info.gw.addr = static_cast<uint32_t>(manual_ip->gateway);
+  info.netmask.addr = static_cast<uint32_t>(manual_ip->subnet);
+
+  esp_err_t dhcp_stop_ret = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA);
+  if (dhcp_stop_ret != ESP_OK) {
+    ESP_LOGV(TAG, "Stopping DHCP client failed! %d", dhcp_stop_ret);
+  }
+
+  esp_err_t wifi_set_info_ret = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &info);
+  if (wifi_set_info_ret != ESP_OK) {
+    ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(wifi_set_info_ret));
+  }
+
+  ip_addr_t dns;
+  dns.type = IPADDR_TYPE_V4;
+  if (uint32_t(manual_ip->dns1) != 0) {
+    dns.u_addr.ip4.addr = static_cast<uint32_t>(manual_ip->dns1);
+    dns_setserver(0, &dns);
+  }
+  if (uint32_t(manual_ip->dns2) != 0) {
+    dns.u_addr.ip4.addr = static_cast<uint32_t>(manual_ip->dns2);
+    dns_setserver(1, &dns);
+  }
+
+  return true;
+}
+
+IPAddress WiFiComponent::wifi_sta_ip_() {
+  if (!this->has_sta())
+    return IPAddress();
+  tcpip_adapter_ip_info_t ip;
+  tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip);
+  return IPAddress(ip.ip.addr);
+}
+
+bool WiFiComponent::wifi_apply_hostname_() {
+  esp_err_t err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str());
+  if (err != ESP_OK) {
+    ESP_LOGV(TAG, "Setting hostname failed: %d", err);
+    return false;
+  }
+  return true;
+}
+bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) {
+  // enable STA
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  wifi_config_t conf;
+  memset(&conf, 0, sizeof(conf));
+  strcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str());
+  strcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str());
+
+  if (ap.get_bssid().has_value()) {
+    conf.sta.bssid_set = 1;
+    memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6);
+  } else {
+    conf.sta.bssid_set = 0;
+  }
+  if (ap.get_channel().has_value()) {
+    conf.sta.channel = *ap.get_channel();
+  }
+
+  wifi_config_t current_conf;
+  esp_err_t err;
+  esp_wifi_get_config(WIFI_IF_STA, &current_conf);
+
+  if (memcmp(&current_conf, &conf, sizeof(wifi_config_t)) != 0) {
+    err = esp_wifi_disconnect();
+    if (err != ESP_OK) {
+      ESP_LOGV(TAG, "esp_wifi_disconnect failed! %d", err);
+      return false;
+    }
+  }
+
+  err = esp_wifi_set_config(WIFI_IF_STA, &conf);
+  if (err != ESP_OK) {
+    ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err);
+  }
+
+  if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
+    return false;
+  }
+
+  // setup enterprise authentication if required
+#ifdef ESPHOME_WIFI_WPA2_EAP
+  if (ap.get_eap().has_value()) {
+    // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
+    EAPAuth eap = ap.get_eap().value();
+    err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
+    if (err != ESP_OK) {
+      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err);
+    }
+    int ca_cert_len = strlen(eap.ca_cert);
+    int client_cert_len = strlen(eap.client_cert);
+    int client_key_len = strlen(eap.client_key);
+    if (ca_cert_len) {
+      err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
+      if (err != ESP_OK) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err);
+      }
+    }
+    // workout what type of EAP this is
+    // validation is not required as the config tool has already validated it
+    if (client_cert_len && client_key_len) {
+      // if we have certs, this must be EAP-TLS
+      err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1,
+                                               (uint8_t *) eap.client_key, client_key_len + 1,
+                                               (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
+      if (err != ESP_OK) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err);
+      }
+    } else {
+      // in the absence of certs, assume this is username/password based
+      err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
+      if (err != ESP_OK) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err);
+      }
+      err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
+      if (err != ESP_OK) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err);
+      }
+    }
+    esp_wpa2_config_t wpa2_config = WPA2_CONFIG_INIT_DEFAULT();
+    err = esp_wifi_sta_wpa2_ent_enable(&wpa2_config);
+    if (err != ESP_OK) {
+      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err);
+    }
+  }
+#endif  // ESPHOME_WIFI_WPA2_EAP
+
+  this->wifi_apply_hostname_();
+
+  err = esp_wifi_connect();
+  if (err != ESP_OK) {
+    ESP_LOGW(TAG, "esp_wifi_connect failed! %d", err);
+    return false;
+  }
+
+  return true;
+}
+const char *get_auth_mode_str(uint8_t mode) {
+  switch (mode) {
+    case WIFI_AUTH_OPEN:
+      return "OPEN";
+    case WIFI_AUTH_WEP:
+      return "WEP";
+    case WIFI_AUTH_WPA_PSK:
+      return "WPA PSK";
+    case WIFI_AUTH_WPA2_PSK:
+      return "WPA2 PSK";
+    case WIFI_AUTH_WPA_WPA2_PSK:
+      return "WPA/WPA2 PSK";
+    case WIFI_AUTH_WPA2_ENTERPRISE:
+      return "WPA2 Enterprise";
+    default:
+      return "UNKNOWN";
+  }
+}
+std::string format_ip4_addr(const ip4_addr_t &ip) {
+  char buf[20];
+  sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16),
+          uint8_t(ip.addr >> 24));
+  return buf;
+}
+const char *get_op_mode_str(uint8_t mode) {
+  switch (mode) {
+    case WIFI_OFF:
+      return "OFF";
+    case WIFI_STA:
+      return "STA";
+    case WIFI_AP:
+      return "AP";
+    case WIFI_AP_STA:
+      return "AP+STA";
+    default:
+      return "UNKNOWN";
+  }
+}
+const char *get_disconnect_reason_str(uint8_t reason) {
+  switch (reason) {
+    case WIFI_REASON_AUTH_EXPIRE:
+      return "Auth Expired";
+    case WIFI_REASON_AUTH_LEAVE:
+      return "Auth Leave";
+    case WIFI_REASON_ASSOC_EXPIRE:
+      return "Association Expired";
+    case WIFI_REASON_ASSOC_TOOMANY:
+      return "Too Many Associations";
+    case WIFI_REASON_NOT_AUTHED:
+      return "Not Authenticated";
+    case WIFI_REASON_NOT_ASSOCED:
+      return "Not Associated";
+    case WIFI_REASON_ASSOC_LEAVE:
+      return "Association Leave";
+    case WIFI_REASON_ASSOC_NOT_AUTHED:
+      return "Association not Authenticated";
+    case WIFI_REASON_DISASSOC_PWRCAP_BAD:
+      return "Disassociate Power Cap Bad";
+    case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
+      return "Disassociate Supported Channel Bad";
+    case WIFI_REASON_IE_INVALID:
+      return "IE Invalid";
+    case WIFI_REASON_MIC_FAILURE:
+      return "Mic Failure";
+    case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
+      return "4-Way Handshake Timeout";
+    case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
+      return "Group Key Update Timeout";
+    case WIFI_REASON_IE_IN_4WAY_DIFFERS:
+      return "IE In 4-Way Handshake Differs";
+    case WIFI_REASON_GROUP_CIPHER_INVALID:
+      return "Group Cipher Invalid";
+    case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
+      return "Pairwise Cipher Invalid";
+    case WIFI_REASON_AKMP_INVALID:
+      return "AKMP Invalid";
+    case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
+      return "Unsupported RSN IE version";
+    case WIFI_REASON_INVALID_RSN_IE_CAP:
+      return "Invalid RSN IE Cap";
+    case WIFI_REASON_802_1X_AUTH_FAILED:
+      return "802.1x Authentication Failed";
+    case WIFI_REASON_CIPHER_SUITE_REJECTED:
+      return "Cipher Suite Rejected";
+    case WIFI_REASON_BEACON_TIMEOUT:
+      return "Beacon Timeout";
+    case WIFI_REASON_NO_AP_FOUND:
+      return "AP Not Found";
+    case WIFI_REASON_AUTH_FAIL:
+      return "Authentication Failed";
+    case WIFI_REASON_ASSOC_FAIL:
+      return "Association Failed";
+    case WIFI_REASON_HANDSHAKE_TIMEOUT:
+      return "Handshake Failed";
+    case WIFI_REASON_UNSPECIFIED:
+    default:
+      return "Unspecified";
+  }
+}
+void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_info_t info) {
+  switch (event) {
+    case SYSTEM_EVENT_WIFI_READY: {
+      ESP_LOGV(TAG, "Event: WiFi ready");
+      break;
+    }
+    case SYSTEM_EVENT_SCAN_DONE: {
+      auto it = info.scan_done;
+      ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
+      break;
+    }
+    case SYSTEM_EVENT_STA_START: {
+      ESP_LOGV(TAG, "Event: WiFi STA start");
+      break;
+    }
+    case SYSTEM_EVENT_STA_STOP: {
+      ESP_LOGV(TAG, "Event: WiFi STA stop");
+      break;
+    }
+    case SYSTEM_EVENT_STA_CONNECTED: {
+      auto it = info.connected;
+      char buf[33];
+      memcpy(buf, it.ssid, it.ssid_len);
+      buf[it.ssid_len] = '\0';
+      ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
+               format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
+      break;
+    }
+    case SYSTEM_EVENT_STA_DISCONNECTED: {
+      auto it = info.disconnected;
+      char buf[33];
+      memcpy(buf, it.ssid, it.ssid_len);
+      buf[it.ssid_len] = '\0';
+      if (it.reason == WIFI_REASON_NO_AP_FOUND) {
+        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
+      } else {
+        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
+                 format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
+      }
+      break;
+    }
+    case SYSTEM_EVENT_STA_AUTHMODE_CHANGE: {
+      auto it = info.auth_change;
+      ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode),
+               get_auth_mode_str(it.new_mode));
+      // Mitigate CVE-2020-12638
+      // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
+      if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
+        ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting...");
+        // we can't call retry_connect() from this context, so disconnect immediately
+        // and notify main thread with error_from_callback_
+        err_t err = esp_wifi_disconnect();
+        if (err != ESP_OK) {
+          ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err));
+        }
+        this->error_from_callback_ = true;
+      }
+      break;
+    }
+    case SYSTEM_EVENT_STA_GOT_IP: {
+      auto it = info.got_ip.ip_info;
+      ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(),
+               format_ip4_addr(it.gw).c_str());
+      break;
+    }
+    case SYSTEM_EVENT_STA_LOST_IP: {
+      ESP_LOGV(TAG, "Event: Lost IP");
+      break;
+    }
+    case SYSTEM_EVENT_AP_START: {
+      ESP_LOGV(TAG, "Event: WiFi AP start");
+      break;
+    }
+    case SYSTEM_EVENT_AP_STOP: {
+      ESP_LOGV(TAG, "Event: WiFi AP stop");
+      break;
+    }
+    case SYSTEM_EVENT_AP_STACONNECTED: {
+      auto it = info.sta_connected;
+      ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
+      break;
+    }
+    case SYSTEM_EVENT_AP_STADISCONNECTED: {
+      auto it = info.sta_disconnected;
+      ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
+      break;
+    }
+    case SYSTEM_EVENT_AP_STAIPASSIGNED: {
+      ESP_LOGV(TAG, "Event: AP client assigned IP");
+      break;
+    }
+    case SYSTEM_EVENT_AP_PROBEREQRECVED: {
+      auto it = info.ap_probereqrecved;
+      ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
+      break;
+    }
+    default:
+      break;
+  }
+
+  if (event == SYSTEM_EVENT_STA_DISCONNECTED) {
+    uint8_t reason = info.disconnected.reason;
+    if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT ||
+        reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
+        reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
+      err_t err = esp_wifi_disconnect();
+      if (err != ESP_OK) {
+        ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err));
+      }
+      this->error_from_callback_ = true;
+    }
+  }
+  if (event == SYSTEM_EVENT_SCAN_DONE) {
+    this->wifi_scan_done_callback_();
+  }
+}
+void WiFiComponent::wifi_pre_setup_() {
+  auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
+  WiFi.onEvent(f);
+  WiFi.persistent(false);
+  // Make sure WiFi is in clean state before anything starts
+  this->wifi_mode_(false, false);
+}
+wl_status_t WiFiComponent::wifi_sta_status_() { return WiFi.status(); }
+bool WiFiComponent::wifi_scan_start_() {
+  // enable STA
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  // need to use WiFi because of WiFiScanClass allocations :(
+  int16_t err = WiFi.scanNetworks(true, true, false, 200);
+  if (err != WIFI_SCAN_RUNNING) {
+    ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err);
+    return false;
+  }
+
+  return true;
+}
+void WiFiComponent::wifi_scan_done_callback_() {
+  this->scan_result_.clear();
+
+  int16_t num = WiFi.scanComplete();
+  if (num < 0)
+    return;
+
+  this->scan_result_.reserve(static_cast<unsigned int>(num));
+  for (int i = 0; i < num; i++) {
+    String ssid = WiFi.SSID(i);
+    wifi_auth_mode_t authmode = WiFi.encryptionType(i);
+    int32_t rssi = WiFi.RSSI(i);
+    uint8_t *bssid = WiFi.BSSID(i);
+    int32_t channel = WiFi.channel(i);
+
+    WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()),
+                        channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0);
+    this->scan_result_.push_back(scan);
+  }
+  WiFi.scanDelete();
+  this->scan_done_ = true;
+}
+bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
+  esp_err_t err;
+
+  // enable AP
+  if (!this->wifi_mode_({}, true))
+    return false;
+
+  tcpip_adapter_ip_info_t info;
+  memset(&info, 0, sizeof(info));
+  if (manual_ip.has_value()) {
+    info.ip.addr = static_cast<uint32_t>(manual_ip->static_ip);
+    info.gw.addr = static_cast<uint32_t>(manual_ip->gateway);
+    info.netmask.addr = static_cast<uint32_t>(manual_ip->subnet);
+  } else {
+    info.ip.addr = static_cast<uint32_t>(IPAddress(192, 168, 4, 1));
+    info.gw.addr = static_cast<uint32_t>(IPAddress(192, 168, 4, 1));
+    info.netmask.addr = static_cast<uint32_t>(IPAddress(255, 255, 255, 0));
+  }
+  tcpip_adapter_dhcp_status_t dhcp_status;
+  tcpip_adapter_dhcps_get_status(TCPIP_ADAPTER_IF_AP, &dhcp_status);
+  err = tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP);
+  if (err != ESP_OK) {
+    ESP_LOGV(TAG, "tcpip_adapter_dhcps_stop failed! %d", err);
+    return false;
+  }
+
+  err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info);
+  if (err != ESP_OK) {
+    ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed! %d", err);
+    return false;
+  }
+
+  dhcps_lease_t lease;
+  lease.enable = true;
+  IPAddress start_address = info.ip.addr;
+  start_address[3] += 99;
+  lease.start_ip.addr = static_cast<uint32_t>(start_address);
+  ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.toString().c_str());
+  start_address[3] += 100;
+  lease.end_ip.addr = static_cast<uint32_t>(start_address);
+  ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.toString().c_str());
+  err = tcpip_adapter_dhcps_option(TCPIP_ADAPTER_OP_SET, TCPIP_ADAPTER_REQUESTED_IP_ADDRESS, &lease, sizeof(lease));
+
+  if (err != ESP_OK) {
+    ESP_LOGV(TAG, "tcpip_adapter_dhcps_option failed! %d", err);
+    return false;
+  }
+
+  err = tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP);
+
+  if (err != ESP_OK) {
+    ESP_LOGV(TAG, "tcpip_adapter_dhcps_start failed! %d", err);
+    return false;
+  }
+
+  return true;
+}
+bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
+  // enable AP
+  if (!this->wifi_mode_({}, true))
+    return false;
+
+  wifi_config_t conf;
+  memset(&conf, 0, sizeof(conf));
+  strcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str());
+  conf.ap.channel = ap.get_channel().value_or(1);
+  conf.ap.ssid_hidden = ap.get_ssid().size();
+  conf.ap.max_connection = 5;
+  conf.ap.beacon_interval = 100;
+
+  if (ap.get_password().empty()) {
+    conf.ap.authmode = WIFI_AUTH_OPEN;
+    *conf.ap.password = 0;
+  } else {
+    conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
+    strcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str());
+  }
+
+  esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf);
+  if (err != ESP_OK) {
+    ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err);
+    return false;
+  }
+
+  yield();
+
+  if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
+    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!");
+    return false;
+  }
+
+  return true;
+}
+IPAddress WiFiComponent::wifi_soft_ap_ip() {
+  tcpip_adapter_ip_info_t ip;
+  tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip);
+  return IPAddress(ip.ip.addr);
+}
+bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
+
+}  // namespace wifi
+}  // namespace esphome
+
+#endif

+ 731 - 0
livingroom/src/esphome/components/wifi/wifi_component_esp8266.cpp

@@ -0,0 +1,731 @@
+#include "wifi_component.h"
+
+#ifdef ARDUINO_ARCH_ESP8266
+
+#include <user_interface.h>
+
+#include <utility>
+#include <algorithm>
+#ifdef ESPHOME_WIFI_WPA2_EAP
+#include <wpa2_enterprise.h>
+#endif
+
+extern "C" {
+#include "lwip/err.h"
+#include "lwip/dns.h"
+#include "lwip/dhcp.h"
+#include "lwip/init.h"  // LWIP_VERSION_
+#if LWIP_IPV6
+#include "lwip/netif.h"  // struct netif
+#endif
+}
+
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+#include "esphome/core/esphal.h"
+#include "esphome/core/util.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+namespace wifi {
+
+static const char *TAG = "wifi_esp8266";
+
+bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
+  uint8_t current_mode = wifi_get_opmode();
+  bool current_sta = current_mode & 0b01;
+  bool current_ap = current_mode & 0b10;
+  bool target_sta = sta.value_or(current_sta);
+  bool target_ap = ap.value_or(current_ap);
+  if (current_sta == target_sta && current_ap == target_ap)
+    return true;
+
+  if (target_sta && !current_sta) {
+    ESP_LOGV(TAG, "Enabling STA.");
+  } else if (!target_sta && current_sta) {
+    ESP_LOGV(TAG, "Disabling STA.");
+    // Stop DHCP client when disabling STA
+    // See https://github.com/esp8266/Arduino/pull/5703
+    wifi_station_dhcpc_stop();
+  }
+  if (target_ap && !current_ap) {
+    ESP_LOGV(TAG, "Enabling AP.");
+  } else if (!target_ap && current_ap) {
+    ESP_LOGV(TAG, "Disabling AP.");
+  }
+
+  ETS_UART_INTR_DISABLE();
+  uint8_t mode = 0;
+  if (target_sta)
+    mode |= 0b01;
+  if (target_ap)
+    mode |= 0b10;
+  bool ret = wifi_set_opmode_current(mode);
+  ETS_UART_INTR_ENABLE();
+
+  if (!ret) {
+    ESP_LOGW(TAG, "Setting WiFi mode failed!");
+  }
+
+  return ret;
+}
+bool WiFiComponent::wifi_apply_power_save_() {
+  sleep_type_t power_save;
+  switch (this->power_save_) {
+    case WIFI_POWER_SAVE_LIGHT:
+      power_save = LIGHT_SLEEP_T;
+      break;
+    case WIFI_POWER_SAVE_HIGH:
+      power_save = MODEM_SLEEP_T;
+      break;
+    case WIFI_POWER_SAVE_NONE:
+    default:
+      power_save = NONE_SLEEP_T;
+      break;
+  }
+  return wifi_set_sleep_type(power_save);
+}
+
+#if LWIP_VERSION_MAJOR != 1
+/*
+  lwip v2 needs to be notified of IP changes, see also
+  https://github.com/d-a-v/Arduino/blob/0e7d21e17144cfc5f53c016191daca8723e89ee8/libraries/ESP8266WiFi/src/ESP8266WiFiSTA.cpp#L251
+ */
+#undef netif_set_addr  // need to call lwIP-v1.4 netif_set_addr()
+extern "C" {
+struct netif *eagle_lwip_getif(int netif_index);
+void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t *netmask, const ip4_addr_t *gw);
+};
+#endif
+
+bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
+  // enable STA
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  enum dhcp_status dhcp_status = wifi_station_dhcpc_status();
+  if (!manual_ip.has_value()) {
+    // Use DHCP client
+    if (dhcp_status != DHCP_STARTED) {
+      bool ret = wifi_station_dhcpc_start();
+      if (!ret) {
+        ESP_LOGV(TAG, "Starting DHCP client failed!");
+      }
+      return ret;
+    }
+    return true;
+  }
+
+  bool ret = true;
+
+#if LWIP_VERSION_MAJOR != 1
+  // get current->previous IP address
+  // (check below)
+  ip_info previp{};
+  wifi_get_ip_info(STATION_IF, &previp);
+#endif
+
+  struct ip_info info {};
+  info.ip.addr = static_cast<uint32_t>(manual_ip->static_ip);
+  info.gw.addr = static_cast<uint32_t>(manual_ip->gateway);
+  info.netmask.addr = static_cast<uint32_t>(manual_ip->subnet);
+
+  if (dhcp_status == DHCP_STARTED) {
+    bool dhcp_stop_ret = wifi_station_dhcpc_stop();
+    if (!dhcp_stop_ret) {
+      ESP_LOGV(TAG, "Stopping DHCP client failed!");
+      ret = false;
+    }
+  }
+  bool wifi_set_info_ret = wifi_set_ip_info(STATION_IF, &info);
+  if (!wifi_set_info_ret) {
+    ESP_LOGV(TAG, "Setting manual IP info failed!");
+    ret = false;
+  }
+
+  ip_addr_t dns;
+  if (uint32_t(manual_ip->dns1) != 0) {
+    dns.addr = static_cast<uint32_t>(manual_ip->dns1);
+    dns_setserver(0, &dns);
+  }
+  if (uint32_t(manual_ip->dns2) != 0) {
+    dns.addr = static_cast<uint32_t>(manual_ip->dns2);
+    dns_setserver(1, &dns);
+  }
+
+#if LWIP_VERSION_MAJOR != 1
+  // trigger address change by calling lwIP-v1.4 api
+  // only when ip is already set by other mean (generally dhcp)
+  if (previp.ip.addr != 0 && previp.ip.addr != info.ip.addr) {
+    netif_set_addr(eagle_lwip_getif(STATION_IF), reinterpret_cast<const ip4_addr_t *>(&info.ip),
+                   reinterpret_cast<const ip4_addr_t *>(&info.netmask), reinterpret_cast<const ip4_addr_t *>(&info.gw));
+  }
+#endif
+  return ret;
+}
+
+IPAddress WiFiComponent::wifi_sta_ip_() {
+  if (!this->has_sta())
+    return {};
+  struct ip_info ip {};
+  wifi_get_ip_info(STATION_IF, &ip);
+  return {ip.ip.addr};
+}
+bool WiFiComponent::wifi_apply_hostname_() {
+  const std::string &hostname = App.get_name();
+  bool ret = wifi_station_set_hostname(const_cast<char *>(hostname.c_str()));
+  if (!ret) {
+    ESP_LOGV(TAG, "Setting WiFi Hostname failed!");
+  }
+
+  // inform dhcp server of hostname change using dhcp_renew()
+  for (netif *intf = netif_list; intf; intf = intf->next) {
+    // unconditionally update all known interfaces
+#if LWIP_VERSION_MAJOR == 1
+    intf->hostname = (char *) wifi_station_get_hostname();
+#else
+    intf->hostname = wifi_station_get_hostname();
+#endif
+    if (netif_dhcp_data(intf) != nullptr) {
+      // renew already started DHCP leases
+      err_t lwipret = dhcp_renew(intf);
+      if (lwipret != ERR_OK) {
+        ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname,
+                 (int) lwipret, intf->name[0], intf->name[1], intf->num);
+        ret = false;
+      }
+    }
+  }
+
+  return ret;
+}
+
+bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) {
+  // enable STA
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  this->wifi_disconnect_();
+
+  struct station_config conf {};
+  memset(&conf, 0, sizeof(conf));
+  strcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str());
+  strcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str());
+
+  if (ap.get_bssid().has_value()) {
+    conf.bssid_set = 1;
+    memcpy(conf.bssid, ap.get_bssid()->data(), 6);
+  } else {
+    conf.bssid_set = 0;
+  }
+
+#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
+  if (ap.get_password().empty()) {
+    conf.threshold.authmode = AUTH_OPEN;
+  } else {
+    // Only allow auth modes with at least WPA
+    conf.threshold.authmode = AUTH_WPA_PSK;
+  }
+  conf.threshold.rssi = -127;
+#endif
+
+  ETS_UART_INTR_DISABLE();
+  bool ret = wifi_station_set_config_current(&conf);
+  ETS_UART_INTR_ENABLE();
+
+  if (!ret) {
+    ESP_LOGV(TAG, "Setting WiFi Station config failed!");
+    return false;
+  }
+
+  if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
+    return false;
+  }
+
+  // setup enterprise authentication if required
+#ifdef ESPHOME_WIFI_WPA2_EAP
+  if (ap.get_eap().has_value()) {
+    // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
+    EAPAuth eap = ap.get_eap().value();
+    ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
+    if (ret) {
+      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", ret);
+    }
+    int ca_cert_len = strlen(eap.ca_cert);
+    int client_cert_len = strlen(eap.client_cert);
+    int client_key_len = strlen(eap.client_key);
+    if (ca_cert_len) {
+      ret = wifi_station_set_enterprise_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
+      if (ret) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", ret);
+      }
+    }
+    // workout what type of EAP this is
+    // validation is not required as the config tool has already validated it
+    if (client_cert_len && client_key_len) {
+      // if we have certs, this must be EAP-TLS
+      ret = wifi_station_set_enterprise_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1,
+                                                 (uint8_t *) eap.client_key, client_key_len + 1,
+                                                 (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
+      if (ret) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", ret);
+      }
+    } else {
+      // in the absence of certs, assume this is username/password based
+      ret = wifi_station_set_enterprise_username((uint8_t *) eap.username.c_str(), eap.username.length());
+      if (ret) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", ret);
+      }
+      ret = wifi_station_set_enterprise_password((uint8_t *) eap.password.c_str(), eap.password.length());
+      if (ret) {
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", ret);
+      }
+    }
+    ret = wifi_station_set_wpa2_enterprise_auth(true);
+    if (ret) {
+      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret);
+    }
+  }
+#endif  // ESPHOME_WIFI_WPA2_EAP
+
+  this->wifi_apply_hostname_();
+
+  ETS_UART_INTR_DISABLE();
+  ret = wifi_station_connect();
+  ETS_UART_INTR_ENABLE();
+  if (!ret) {
+    ESP_LOGV(TAG, "wifi_station_connect failed!");
+    return false;
+  }
+
+  if (ap.get_channel().has_value()) {
+    ret = wifi_set_channel(*ap.get_channel());
+    if (!ret) {
+      ESP_LOGV(TAG, "wifi_set_channel failed!");
+      return false;
+    }
+  }
+
+  return true;
+}
+
+class WiFiMockClass : public ESP8266WiFiGenericClass {
+ public:
+  static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); }  // NOLINT
+};
+
+const char *get_auth_mode_str(uint8_t mode) {
+  switch (mode) {
+    case AUTH_OPEN:
+      return "OPEN";
+    case AUTH_WEP:
+      return "WEP";
+    case AUTH_WPA_PSK:
+      return "WPA PSK";
+    case AUTH_WPA2_PSK:
+      return "WPA2 PSK";
+    case AUTH_WPA_WPA2_PSK:
+      return "WPA/WPA2 PSK";
+    default:
+      return "UNKNOWN";
+  }
+}
+#ifdef ipv4_addr
+std::string format_ip_addr(struct ipv4_addr ip) {
+  char buf[20];
+  sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16),
+          uint8_t(ip.addr >> 24));
+  return buf;
+}
+#else
+std::string format_ip_addr(struct ip_addr ip) {
+  char buf[20];
+  sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16),
+          uint8_t(ip.addr >> 24));
+  return buf;
+}
+#endif
+const char *get_op_mode_str(uint8_t mode) {
+  switch (mode) {
+    case WIFI_OFF:
+      return "OFF";
+    case WIFI_STA:
+      return "STA";
+    case WIFI_AP:
+      return "AP";
+    case WIFI_AP_STA:
+      return "AP+STA";
+    default:
+      return "UNKNOWN";
+  }
+}
+const char *get_disconnect_reason_str(uint8_t reason) {
+  switch (reason) {
+    case REASON_AUTH_EXPIRE:
+      return "Auth Expired";
+    case REASON_AUTH_LEAVE:
+      return "Auth Leave";
+    case REASON_ASSOC_EXPIRE:
+      return "Association Expired";
+    case REASON_ASSOC_TOOMANY:
+      return "Too Many Associations";
+    case REASON_NOT_AUTHED:
+      return "Not Authenticated";
+    case REASON_NOT_ASSOCED:
+      return "Not Associated";
+    case REASON_ASSOC_LEAVE:
+      return "Association Leave";
+    case REASON_ASSOC_NOT_AUTHED:
+      return "Association not Authenticated";
+    case REASON_DISASSOC_PWRCAP_BAD:
+      return "Disassociate Power Cap Bad";
+    case REASON_DISASSOC_SUPCHAN_BAD:
+      return "Disassociate Supported Channel Bad";
+    case REASON_IE_INVALID:
+      return "IE Invalid";
+    case REASON_MIC_FAILURE:
+      return "Mic Failure";
+    case REASON_4WAY_HANDSHAKE_TIMEOUT:
+      return "4-Way Handshake Timeout";
+    case REASON_GROUP_KEY_UPDATE_TIMEOUT:
+      return "Group Key Update Timeout";
+    case REASON_IE_IN_4WAY_DIFFERS:
+      return "IE In 4-Way Handshake Differs";
+    case REASON_GROUP_CIPHER_INVALID:
+      return "Group Cipher Invalid";
+    case REASON_PAIRWISE_CIPHER_INVALID:
+      return "Pairwise Cipher Invalid";
+    case REASON_AKMP_INVALID:
+      return "AKMP Invalid";
+    case REASON_UNSUPP_RSN_IE_VERSION:
+      return "Unsupported RSN IE version";
+    case REASON_INVALID_RSN_IE_CAP:
+      return "Invalid RSN IE Cap";
+    case REASON_802_1X_AUTH_FAILED:
+      return "802.1x Authentication Failed";
+    case REASON_CIPHER_SUITE_REJECTED:
+      return "Cipher Suite Rejected";
+    case REASON_BEACON_TIMEOUT:
+      return "Beacon Timeout";
+    case REASON_NO_AP_FOUND:
+      return "AP Not Found";
+    case REASON_AUTH_FAIL:
+      return "Authentication Failed";
+    case REASON_ASSOC_FAIL:
+      return "Association Failed";
+    case REASON_HANDSHAKE_TIMEOUT:
+      return "Handshake Failed";
+    case REASON_UNSPECIFIED:
+    default:
+      return "Unspecified";
+  }
+}
+
+void WiFiComponent::wifi_event_callback(System_Event_t *event) {
+  switch (event->event) {
+    case EVENT_STAMODE_CONNECTED: {
+      auto it = event->event_info.connected;
+      char buf[33];
+      memcpy(buf, it.ssid, it.ssid_len);
+      buf[it.ssid_len] = '\0';
+      ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(),
+               it.channel);
+      break;
+    }
+    case EVENT_STAMODE_DISCONNECTED: {
+      auto it = event->event_info.disconnected;
+      char buf[33];
+      memcpy(buf, it.ssid, it.ssid_len);
+      buf[it.ssid_len] = '\0';
+      if (it.reason == REASON_NO_AP_FOUND) {
+        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
+      } else {
+        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
+                 format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
+      }
+      break;
+    }
+    case EVENT_STAMODE_AUTHMODE_CHANGE: {
+      auto it = event->event_info.auth_change;
+      ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", get_auth_mode_str(it.old_mode),
+               get_auth_mode_str(it.new_mode));
+      // Mitigate CVE-2020-12638
+      // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
+      if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) {
+        ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting...");
+        // we can't call retry_connect() from this context, so disconnect immediately
+        // and notify main thread with error_from_callback_
+        wifi_station_disconnect();
+        global_wifi_component->error_from_callback_ = true;
+      }
+      break;
+    }
+    case EVENT_STAMODE_GOT_IP: {
+      auto it = event->event_info.got_ip;
+      ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(),
+               format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str());
+      break;
+    }
+    case EVENT_STAMODE_DHCP_TIMEOUT: {
+      ESP_LOGW(TAG, "Event: Getting IP address timeout");
+      break;
+    }
+    case EVENT_SOFTAPMODE_STACONNECTED: {
+      auto it = event->event_info.sta_connected;
+      ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
+      break;
+    }
+    case EVENT_SOFTAPMODE_STADISCONNECTED: {
+      auto it = event->event_info.sta_disconnected;
+      ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
+      break;
+    }
+    case EVENT_SOFTAPMODE_PROBEREQRECVED: {
+      auto it = event->event_info.ap_probereqrecved;
+      ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
+      break;
+    }
+#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
+    case EVENT_OPMODE_CHANGED: {
+      auto it = event->event_info.opmode_changed;
+      ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", get_op_mode_str(it.old_opmode),
+               get_op_mode_str(it.new_opmode));
+      break;
+    }
+    case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: {
+      auto it = event->event_info.distribute_sta_ip;
+      ESP_LOGV(TAG, "Event: AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(),
+               format_ip_addr(it.ip).c_str(), it.aid);
+      break;
+    }
+#endif
+    default:
+      break;
+  }
+
+  if (event->event == EVENT_STAMODE_DISCONNECTED) {
+    global_wifi_component->error_from_callback_ = true;
+  }
+
+  WiFiMockClass::_event_callback(event);
+}
+
+bool WiFiComponent::wifi_apply_output_power_(float output_power) {
+  uint8_t val = static_cast<uint8_t>(output_power * 4);
+  system_phy_set_max_tpw(val);
+  return true;
+}
+bool WiFiComponent::wifi_sta_pre_setup_() {
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  bool ret1, ret2;
+  ETS_UART_INTR_DISABLE();
+  ret1 = wifi_station_set_auto_connect(0);
+  ret2 = wifi_station_set_reconnect_policy(false);
+  ETS_UART_INTR_ENABLE();
+
+  if (!ret1 || !ret2) {
+    ESP_LOGV(TAG, "Disabling Auto-Connect failed!");
+  }
+
+  delay(10);
+  return true;
+}
+
+void WiFiComponent::wifi_pre_setup_() {
+  wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback);
+
+  // Make sure WiFi is in clean state before anything starts
+  this->wifi_mode_(false, false);
+}
+
+wl_status_t WiFiComponent::wifi_sta_status_() {
+  station_status_t status = wifi_station_get_connect_status();
+  switch (status) {
+    case STATION_GOT_IP:
+      return WL_CONNECTED;
+    case STATION_NO_AP_FOUND:
+      return WL_NO_SSID_AVAIL;
+    case STATION_CONNECT_FAIL:
+    case STATION_WRONG_PASSWORD:
+      return WL_CONNECT_FAILED;
+    case STATION_IDLE:
+      return WL_IDLE_STATUS;
+    case STATION_CONNECTING:
+    default:
+      return WL_DISCONNECTED;
+  }
+}
+bool WiFiComponent::wifi_scan_start_() {
+  static bool FIRST_SCAN = false;
+
+  // enable STA
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  struct scan_config config {};
+  memset(&config, 0, sizeof(config));
+  config.ssid = nullptr;
+  config.bssid = nullptr;
+  config.channel = 0;
+  config.show_hidden = 1;
+#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
+  config.scan_type = WIFI_SCAN_TYPE_ACTIVE;
+  if (FIRST_SCAN) {
+    config.scan_time.active.min = 100;
+    config.scan_time.active.max = 200;
+  } else {
+    config.scan_time.active.min = 400;
+    config.scan_time.active.max = 500;
+  }
+#endif
+  FIRST_SCAN = false;
+  bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
+  if (!ret) {
+    ESP_LOGV(TAG, "wifi_station_scan failed!");
+    return false;
+  }
+
+  return ret;
+}
+bool WiFiComponent::wifi_disconnect_() {
+  bool ret = true;
+  // Only call disconnect if interface is up
+  if (wifi_get_opmode() & WIFI_STA)
+    ret = wifi_station_disconnect();
+  station_config conf{};
+  memset(&conf, 0, sizeof(conf));
+  ETS_UART_INTR_DISABLE();
+  wifi_station_set_config_current(&conf);
+  ETS_UART_INTR_ENABLE();
+  return ret;
+}
+void WiFiComponent::s_wifi_scan_done_callback(void *arg, STATUS status) {
+  global_wifi_component->wifi_scan_done_callback_(arg, status);
+}
+
+void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
+  this->scan_result_.clear();
+
+  if (status != OK) {
+    ESP_LOGV(TAG, "Scan failed! %d", status);
+    this->retry_connect();
+    return;
+  }
+  auto *head = reinterpret_cast<bss_info *>(arg);
+  for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
+    WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
+                       std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,
+                       it->authmode != AUTH_OPEN, it->is_hidden != 0);
+    this->scan_result_.push_back(res);
+  }
+  this->scan_done_ = true;
+}
+bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
+  // enable AP
+  if (!this->wifi_mode_({}, true))
+    return false;
+
+  struct ip_info info {};
+  if (manual_ip.has_value()) {
+    info.ip.addr = static_cast<uint32_t>(manual_ip->static_ip);
+    info.gw.addr = static_cast<uint32_t>(manual_ip->gateway);
+    info.netmask.addr = static_cast<uint32_t>(manual_ip->subnet);
+  } else {
+    info.ip.addr = static_cast<uint32_t>(IPAddress(192, 168, 4, 1));
+    info.gw.addr = static_cast<uint32_t>(IPAddress(192, 168, 4, 1));
+    info.netmask.addr = static_cast<uint32_t>(IPAddress(255, 255, 255, 0));
+  }
+
+  if (wifi_softap_dhcps_status() == DHCP_STARTED) {
+    if (!wifi_softap_dhcps_stop()) {
+      ESP_LOGV(TAG, "Stopping DHCP server failed!");
+    }
+  }
+
+  if (!wifi_set_ip_info(SOFTAP_IF, &info)) {
+    ESP_LOGV(TAG, "Setting SoftAP info failed!");
+    return false;
+  }
+
+  struct dhcps_lease lease {};
+  IPAddress start_address = info.ip.addr;
+  start_address[3] += 99;
+  lease.start_ip.addr = static_cast<uint32_t>(start_address);
+  ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.toString().c_str());
+  start_address[3] += 100;
+  lease.end_ip.addr = static_cast<uint32_t>(start_address);
+  ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.toString().c_str());
+  if (!wifi_softap_set_dhcps_lease(&lease)) {
+    ESP_LOGV(TAG, "Setting SoftAP DHCP lease failed!");
+    return false;
+  }
+
+  // lease time 1440 minutes (=24 hours)
+  if (!wifi_softap_set_dhcps_lease_time(1440)) {
+    ESP_LOGV(TAG, "Setting SoftAP DHCP lease time failed!");
+    return false;
+  }
+
+  uint8_t mode = 1;
+  // bit0, 1 enables router information from ESP8266 SoftAP DHCP server.
+  if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) {
+    ESP_LOGV(TAG, "wifi_softap_set_dhcps_offer_option failed!");
+    return false;
+  }
+
+  if (!wifi_softap_dhcps_start()) {
+    ESP_LOGV(TAG, "Starting SoftAP DHCPS failed!");
+    return false;
+  }
+
+  return true;
+}
+bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
+  // enable AP
+  if (!this->wifi_mode_({}, true))
+    return false;
+
+  struct softap_config conf {};
+  strcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str());
+  conf.ssid_len = static_cast<uint8>(ap.get_ssid().size());
+  conf.channel = ap.get_channel().value_or(1);
+  conf.ssid_hidden = ap.get_hidden();
+  conf.max_connection = 5;
+  conf.beacon_interval = 100;
+
+  if (ap.get_password().empty()) {
+    conf.authmode = AUTH_OPEN;
+    *conf.password = 0;
+  } else {
+    conf.authmode = AUTH_WPA2_PSK;
+    strcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str());
+  }
+
+  ETS_UART_INTR_DISABLE();
+  bool ret = wifi_softap_set_config_current(&conf);
+  ETS_UART_INTR_ENABLE();
+
+  if (!ret) {
+    ESP_LOGV(TAG, "wifi_softap_set_config_current failed!");
+    return false;
+  }
+
+  if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
+    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!");
+    return false;
+  }
+
+  return true;
+}
+IPAddress WiFiComponent::wifi_soft_ap_ip() {
+  struct ip_info ip {};
+  wifi_get_ip_info(SOFTAP_IF, &ip);
+  return {ip.ip.addr};
+}
+
+}  // namespace wifi
+}  // namespace esphome
+
+#endif

+ 163 - 0
livingroom/src/esphome/core/application.cpp

@@ -0,0 +1,163 @@
+#include "esphome/core/application.h"
+#include "esphome/core/log.h"
+#include "esphome/core/version.h"
+#include "esphome/core/esphal.h"
+
+#ifdef USE_STATUS_LED
+#include "esphome/components/status_led/status_led.h"
+#endif
+
+namespace esphome {
+
+static const char *TAG = "app";
+
+void Application::register_component_(Component *comp) {
+  if (comp == nullptr) {
+    ESP_LOGW(TAG, "Tried to register null component!");
+    return;
+  }
+
+  for (auto *c : this->components_) {
+    if (comp == c) {
+      ESP_LOGW(TAG, "Component already registered! (%p)", c);
+      return;
+    }
+  }
+  this->components_.push_back(comp);
+}
+void Application::setup() {
+  ESP_LOGI(TAG, "Running through setup()...");
+  ESP_LOGV(TAG, "Sorting components by setup priority...");
+  std::stable_sort(this->components_.begin(), this->components_.end(), [](const Component *a, const Component *b) {
+    return a->get_actual_setup_priority() > b->get_actual_setup_priority();
+  });
+
+  for (uint32_t i = 0; i < this->components_.size(); i++) {
+    Component *component = this->components_[i];
+
+    component->call();
+    this->scheduler.process_to_add();
+    if (component->can_proceed())
+      continue;
+
+    std::stable_sort(this->components_.begin(), this->components_.begin() + i + 1,
+                     [](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); });
+
+    do {
+      uint32_t new_app_state = STATUS_LED_WARNING;
+      this->scheduler.call();
+      for (uint32_t j = 0; j <= i; j++) {
+        this->components_[j]->call();
+        new_app_state |= this->components_[j]->get_component_state();
+        this->app_state_ |= new_app_state;
+      }
+      this->app_state_ = new_app_state;
+      yield();
+    } while (!component->can_proceed());
+  }
+
+  ESP_LOGI(TAG, "setup() finished successfully!");
+  this->schedule_dump_config();
+  this->calculate_looping_components_();
+
+  // Dummy function to link some symbols into the binary.
+  force_link_symbols();
+}
+void Application::loop() {
+  uint32_t new_app_state = 0;
+  const uint32_t start = millis();
+
+  this->scheduler.call();
+  for (Component *component : this->looping_components_) {
+    component->call();
+    new_app_state |= component->get_component_state();
+    this->app_state_ |= new_app_state;
+    this->feed_wdt();
+  }
+  this->app_state_ = new_app_state;
+
+  const uint32_t end = millis();
+  if (end - start > 200) {
+    ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.2f s).", (end - start) / 1e3f);
+    ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop().");
+  }
+
+  const uint32_t now = millis();
+
+  if (HighFrequencyLoopRequester::is_high_frequency()) {
+    yield();
+  } else {
+    uint32_t delay_time = this->loop_interval_;
+    if (now - this->last_loop_ < this->loop_interval_)
+      delay_time = this->loop_interval_ - (now - this->last_loop_);
+
+    uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time);
+    // next_schedule is max 0.5*delay_time
+    // otherwise interval=0 schedules result in constant looping with almost no sleep
+    next_schedule = std::max(next_schedule, delay_time / 2);
+    delay_time = std::min(next_schedule, delay_time);
+    delay(delay_time);
+  }
+  this->last_loop_ = now;
+
+  if (this->dump_config_at_ >= 0 && this->dump_config_at_ < this->components_.size()) {
+    if (this->dump_config_at_ == 0) {
+      ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str());
+    }
+
+    this->components_[this->dump_config_at_]->dump_config();
+    this->dump_config_at_++;
+  }
+}
+
+void ICACHE_RAM_ATTR HOT Application::feed_wdt() {
+  static uint32_t LAST_FEED = 0;
+  uint32_t now = millis();
+  if (now - LAST_FEED > 3) {
+#ifdef ARDUINO_ARCH_ESP8266
+    ESP.wdtFeed();
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+    yield();
+#endif
+    LAST_FEED = now;
+#ifdef USE_STATUS_LED
+    if (status_led::global_status_led != nullptr) {
+      status_led::global_status_led->call();
+    }
+#endif
+  }
+}
+void Application::reboot() {
+  ESP_LOGI(TAG, "Forcing a reboot...");
+  for (auto *comp : this->components_)
+    comp->on_shutdown();
+  ESP.restart();
+  // restart() doesn't always end execution
+  while (true) {
+    yield();
+  }
+}
+void Application::safe_reboot() {
+  ESP_LOGI(TAG, "Rebooting safely...");
+  for (auto *comp : this->components_)
+    comp->on_safe_shutdown();
+  for (auto *comp : this->components_)
+    comp->on_shutdown();
+  ESP.restart();
+  // restart() doesn't always end execution
+  while (true) {
+    yield();
+  }
+}
+
+void Application::calculate_looping_components_() {
+  for (auto *obj : this->components_) {
+    if (obj->has_overridden_loop())
+      this->looping_components_.push_back(obj);
+  }
+}
+
+Application App;
+
+}  // namespace esphome

+ 253 - 0
livingroom/src/esphome/core/application.h

@@ -0,0 +1,253 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include "esphome/core/defines.h"
+#include "esphome/core/preferences.h"
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/scheduler.h"
+
+#ifdef USE_BINARY_SENSOR
+#include "esphome/components/binary_sensor/binary_sensor.h"
+#endif
+#ifdef USE_SENSOR
+#include "esphome/components/sensor/sensor.h"
+#endif
+#ifdef USE_SWITCH
+#include "esphome/components/switch/switch.h"
+#endif
+#ifdef USE_TEXT_SENSOR
+#include "esphome/components/text_sensor/text_sensor.h"
+#endif
+#ifdef USE_FAN
+#include "esphome/components/fan/fan_state.h"
+#endif
+#ifdef USE_CLIMATE
+#include "esphome/components/climate/climate.h"
+#endif
+#ifdef USE_LIGHT
+#include "esphome/components/light/light_state.h"
+#endif
+#ifdef USE_COVER
+#include "esphome/components/cover/cover.h"
+#endif
+
+namespace esphome {
+
+class Application {
+ public:
+  void pre_setup(const std::string &name, const char *compilation_time) {
+    this->name_ = name;
+    this->compilation_time_ = compilation_time;
+    global_preferences.begin();
+  }
+
+#ifdef USE_BINARY_SENSOR
+  void register_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
+    this->binary_sensors_.push_back(binary_sensor);
+  }
+#endif
+
+#ifdef USE_SENSOR
+  void register_sensor(sensor::Sensor *sensor) { this->sensors_.push_back(sensor); }
+#endif
+
+#ifdef USE_SWITCH
+  void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); }
+#endif
+
+#ifdef USE_TEXT_SENSOR
+  void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
+#endif
+
+#ifdef USE_FAN
+  void register_fan(fan::FanState *state) { this->fans_.push_back(state); }
+#endif
+
+#ifdef USE_COVER
+  void register_cover(cover::Cover *cover) { this->covers_.push_back(cover); }
+#endif
+
+#ifdef USE_CLIMATE
+  void register_climate(climate::Climate *climate) { this->climates_.push_back(climate); }
+#endif
+
+#ifdef USE_LIGHT
+  void register_light(light::LightState *light) { this->lights_.push_back(light); }
+#endif
+
+  /// Register the component in this Application instance.
+  template<class C> C *register_component(C *c) {
+    static_assert(std::is_base_of<Component, C>::value, "Only Component subclasses can be registered");
+    this->register_component_((Component *) c);
+    return c;
+  }
+
+  /// Set up all the registered components. Call this at the end of your setup() function.
+  void setup();
+
+  /// Make a loop iteration. Call this in your loop() function.
+  void loop();
+
+  /// Get the name of this Application set by set_name().
+  const std::string &get_name() const { return this->name_; }
+
+  const std::string &get_compilation_time() const { return this->compilation_time_; }
+
+  /** Set the target interval with which to run the loop() calls.
+   * If the loop() method takes longer than the target interval, ESPHome won't
+   * sleep in loop(), but if the time spent in loop() is small than the target, ESPHome
+   * will delay at the end of the App.loop() method.
+   *
+   * This is done to conserve power: In most use-cases, high-speed loop() calls are not required
+   * and degrade power consumption.
+   *
+   * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester
+   * helper in helpers.h
+   *
+   * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds.
+   */
+  void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; }
+
+  void schedule_dump_config() { this->dump_config_at_ = 0; }
+
+  void feed_wdt();
+
+  void reboot();
+
+  void safe_reboot();
+
+  void run_safe_shutdown_hooks() {
+    for (auto *comp : this->components_) {
+      comp->on_safe_shutdown();
+    }
+    for (auto *comp : this->components_) {
+      comp->on_shutdown();
+    }
+  }
+
+  uint32_t get_app_state() const { return this->app_state_; }
+
+#ifdef USE_BINARY_SENSOR
+  const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; }
+  binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->binary_sensors_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
+#ifdef USE_SWITCH
+  const std::vector<switch_::Switch *> &get_switches() { return this->switches_; }
+  switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->switches_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
+#ifdef USE_SENSOR
+  const std::vector<sensor::Sensor *> &get_sensors() { return this->sensors_; }
+  sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->sensors_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
+#ifdef USE_TEXT_SENSOR
+  const std::vector<text_sensor::TextSensor *> &get_text_sensors() { return this->text_sensors_; }
+  text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->text_sensors_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
+#ifdef USE_FAN
+  const std::vector<fan::FanState *> &get_fans() { return this->fans_; }
+  fan::FanState *get_fan_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->fans_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
+#ifdef USE_COVER
+  const std::vector<cover::Cover *> &get_covers() { return this->covers_; }
+  cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->covers_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
+#ifdef USE_LIGHT
+  const std::vector<light::LightState *> &get_lights() { return this->lights_; }
+  light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->lights_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
+#ifdef USE_CLIMATE
+  const std::vector<climate::Climate *> &get_climates() { return this->climates_; }
+  climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) {
+    for (auto *obj : this->climates_)
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
+        return obj;
+    return nullptr;
+  }
+#endif
+
+  Scheduler scheduler;
+
+ protected:
+  friend Component;
+
+  void register_component_(Component *comp);
+
+  void calculate_looping_components_();
+
+  std::vector<Component *> components_{};
+  std::vector<Component *> looping_components_{};
+
+#ifdef USE_BINARY_SENSOR
+  std::vector<binary_sensor::BinarySensor *> binary_sensors_{};
+#endif
+#ifdef USE_SWITCH
+  std::vector<switch_::Switch *> switches_{};
+#endif
+#ifdef USE_SENSOR
+  std::vector<sensor::Sensor *> sensors_{};
+#endif
+#ifdef USE_TEXT_SENSOR
+  std::vector<text_sensor::TextSensor *> text_sensors_{};
+#endif
+#ifdef USE_FAN
+  std::vector<fan::FanState *> fans_{};
+#endif
+#ifdef USE_COVER
+  std::vector<cover::Cover *> covers_{};
+#endif
+#ifdef USE_CLIMATE
+  std::vector<climate::Climate *> climates_{};
+#endif
+#ifdef USE_LIGHT
+  std::vector<light::LightState *> lights_{};
+#endif
+
+  std::string name_;
+  std::string compilation_time_;
+  uint32_t last_loop_{0};
+  uint32_t loop_interval_{16};
+  int dump_config_at_{-1};
+  uint32_t app_state_{0};
+};
+
+/// Global storage of Application pointer - only one Application can exist.
+extern Application App;
+
+}  // namespace esphome

+ 212 - 0
livingroom/src/esphome/core/automation.h

@@ -0,0 +1,212 @@
+#pragma once
+
+#include <vector>
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/preferences.h"
+
+namespace esphome {
+
+#define TEMPLATABLE_VALUE_(type, name) \
+ protected: \
+  TemplatableValue<type, Ts...> name##_{}; \
+\
+ public: \
+  template<typename V> void set_##name(V name) { this->name##_ = name; }
+
+#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)
+
+#define TEMPLATABLE_STRING_VALUE_(name) \
+ protected: \
+  TemplatableStringValue<Ts...> name##_{}; \
+\
+ public: \
+  template<typename V> void set_##name(V name) { this->name##_ = name; }
+
+#define TEMPLATABLE_STRING_VALUE(name) TEMPLATABLE_STRING_VALUE_(name)
+
+/** Base class for all automation conditions.
+ *
+ * @tparam Ts The template parameters to pass when executing.
+ */
+template<typename... Ts> class Condition {
+ public:
+  /// Check whether this condition passes. This condition check must be instant, and not cause any delays.
+  virtual bool check(Ts... x) = 0;
+
+  /// Call check with a tuple of values as parameter.
+  bool check_tuple(const std::tuple<Ts...> &tuple) {
+    return this->check_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
+  }
+
+ protected:
+  template<int... S> bool check_tuple_(const std::tuple<Ts...> &tuple, seq<S...>) {
+    return this->check(std::get<S>(tuple)...);
+  }
+};
+
+template<typename... Ts> class Automation;
+
+template<typename... Ts> class Trigger {
+ public:
+  /// Inform the parent automation that the event has triggered.
+  void trigger(Ts... x) {
+    if (this->automation_parent_ == nullptr)
+      return;
+    this->automation_parent_->trigger(x...);
+  }
+  void set_automation_parent(Automation<Ts...> *automation_parent) { this->automation_parent_ = automation_parent; }
+
+  /// Stop any action connected to this trigger.
+  void stop_action() {
+    if (this->automation_parent_ == nullptr)
+      return;
+    this->automation_parent_->stop();
+  }
+  /// Returns true if any action connected to this trigger is running.
+  bool is_action_running() {
+    if (this->automation_parent_ == nullptr)
+      return false;
+    return this->automation_parent_->is_running();
+  }
+
+ protected:
+  Automation<Ts...> *automation_parent_{nullptr};
+};
+
+template<typename... Ts> class ActionList;
+
+template<typename... Ts> class Action {
+ public:
+  virtual void play_complex(Ts... x) {
+    this->num_running_++;
+    this->play(x...);
+    this->play_next_(x...);
+  }
+  virtual void stop_complex() {
+    if (num_running_) {
+      this->stop();
+      this->num_running_ = 0;
+    }
+    this->stop_next_();
+  }
+  /// Check if this or any of the following actions are currently running.
+  virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); }
+
+  /// The total number of actions that are currently running in this plus any of
+  /// the following actions in the chain.
+  int num_running_total() {
+    int total = this->num_running_;
+    if (this->next_ != nullptr)
+      total += this->next_->num_running_total();
+    return total;
+  }
+
+ protected:
+  friend ActionList<Ts...>;
+
+  virtual void play(Ts... x) = 0;
+  void play_next_(Ts... x) {
+    if (this->num_running_ > 0) {
+      this->num_running_--;
+      if (this->next_ != nullptr) {
+        this->next_->play_complex(x...);
+      }
+    }
+  }
+  template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...>) {
+    this->play_next_(std::get<S>(tuple)...);
+  }
+  void play_next_tuple_(const std::tuple<Ts...> &tuple) {
+    this->play_next_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
+  }
+
+  virtual void stop() {}
+  void stop_next_() {
+    if (this->next_ != nullptr) {
+      this->next_->stop_complex();
+    }
+  }
+
+  bool is_running_next_() {
+    if (this->next_ == nullptr)
+      return false;
+    return this->next_->is_running();
+  }
+
+  Action<Ts...> *next_ = nullptr;
+
+  /// The number of instances of this sequence in the list of actions
+  /// that is currently being executed.
+  int num_running_{0};
+};
+
+template<typename... Ts> class ActionList {
+ public:
+  void add_action(Action<Ts...> *action) {
+    if (this->actions_end_ == nullptr) {
+      this->actions_begin_ = action;
+    } else {
+      this->actions_end_->next_ = action;
+    }
+    this->actions_end_ = action;
+  }
+  void add_actions(const std::vector<Action<Ts...> *> &actions) {
+    for (auto *action : actions) {
+      this->add_action(action);
+    }
+  }
+  void play(Ts... x) {
+    if (this->actions_begin_ != nullptr)
+      this->actions_begin_->play_complex(x...);
+  }
+  void play_tuple(const std::tuple<Ts...> &tuple) { this->play_tuple_(tuple, typename gens<sizeof...(Ts)>::type()); }
+  void stop() {
+    if (this->actions_begin_ != nullptr)
+      this->actions_begin_->stop_complex();
+  }
+  bool empty() const { return this->actions_begin_ == nullptr; }
+
+  /// Check if any action in this action list is currently running.
+  bool is_running() {
+    if (this->actions_begin_ == nullptr)
+      return false;
+    return this->actions_begin_->is_running();
+  }
+  /// Return the number of actions in this action list that are currently running.
+  int num_running() {
+    if (this->actions_begin_ == nullptr)
+      return false;
+    return this->actions_begin_->num_running_total();
+  }
+
+ protected:
+  template<int... S> void play_tuple_(const std::tuple<Ts...> &tuple, seq<S...>) { this->play(std::get<S>(tuple)...); }
+
+  Action<Ts...> *actions_begin_{nullptr};
+  Action<Ts...> *actions_end_{nullptr};
+};
+
+template<typename... Ts> class Automation {
+ public:
+  explicit Automation(Trigger<Ts...> *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); }
+
+  Action<Ts...> *add_action(Action<Ts...> *action) { this->actions_.add_action(action); }
+  void add_actions(const std::vector<Action<Ts...> *> &actions) { this->actions_.add_actions(actions); }
+
+  void stop() { this->actions_.stop(); }
+
+  void trigger(Ts... x) { this->actions_.play(x...); }
+
+  bool is_running() { return this->actions_.is_running(); }
+
+  /// Return the number of actions in the action part of this automation that are currently running.
+  int num_running() { return this->actions_.num_running(); }
+
+ protected:
+  Trigger<Ts...> *trigger_;
+  ActionList<Ts...> actions_;
+};
+
+}  // namespace esphome

+ 275 - 0
livingroom/src/esphome/core/base_automation.h

@@ -0,0 +1,275 @@
+#pragma once
+
+#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
+
+namespace esphome {
+
+template<typename... Ts> class AndCondition : public Condition<Ts...> {
+ public:
+  explicit AndCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
+  bool check(Ts... x) override {
+    for (auto *condition : this->conditions_) {
+      if (!condition->check(x...))
+        return false;
+    }
+
+    return true;
+  }
+
+ protected:
+  std::vector<Condition<Ts...> *> conditions_;
+};
+
+template<typename... Ts> class OrCondition : public Condition<Ts...> {
+ public:
+  explicit OrCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
+  bool check(Ts... x) override {
+    for (auto *condition : this->conditions_) {
+      if (condition->check(x...))
+        return true;
+    }
+
+    return false;
+  }
+
+ protected:
+  std::vector<Condition<Ts...> *> conditions_;
+};
+
+template<typename... Ts> class NotCondition : public Condition<Ts...> {
+ public:
+  explicit NotCondition(Condition<Ts...> *condition) : condition_(condition) {}
+  bool check(Ts... x) override { return !this->condition_->check(x...); }
+
+ protected:
+  Condition<Ts...> *condition_;
+};
+
+template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
+ public:
+  explicit LambdaCondition(std::function<bool(Ts...)> &&f) : f_(std::move(f)) {}
+  bool check(Ts... x) override { return this->f_(x...); }
+
+ protected:
+  std::function<bool(Ts...)> f_;
+};
+
+template<typename... Ts> class ForCondition : public Condition<Ts...>, public Component {
+ public:
+  explicit ForCondition(Condition<> *condition) : condition_(condition) {}
+
+  TEMPLATABLE_VALUE(uint32_t, time);
+
+  void loop() override { this->check_internal(); }
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  bool check_internal() {
+    bool cond = this->condition_->check();
+    if (!cond)
+      this->last_inactive_ = millis();
+    return cond;
+  }
+
+  bool check(Ts... x) override {
+    if (!this->check_internal())
+      return false;
+    return millis() - this->last_inactive_ >= this->time_.value(x...);
+  }
+
+ protected:
+  Condition<> *condition_;
+  uint32_t last_inactive_{0};
+};
+
+class StartupTrigger : public Trigger<>, public Component {
+ public:
+  explicit StartupTrigger(float setup_priority) : setup_priority_(setup_priority) {}
+  void setup() override { this->trigger(); }
+  float get_setup_priority() const override { return this->setup_priority_; }
+
+ protected:
+  float setup_priority_;
+};
+
+class ShutdownTrigger : public Trigger<>, public Component {
+ public:
+  void on_shutdown() override { this->trigger(); }
+};
+
+class LoopTrigger : public Trigger<>, public Component {
+ public:
+  void loop() override { this->trigger(); }
+  float get_setup_priority() const override { return setup_priority::DATA; }
+};
+
+template<typename... Ts> class DelayAction : public Action<Ts...>, public Component {
+ public:
+  explicit DelayAction() = default;
+
+  TEMPLATABLE_VALUE(uint32_t, delay)
+
+  void play_complex(Ts... x) override {
+    auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
+    this->num_running_++;
+    this->set_timeout(this->delay_.value(x...), f);
+  }
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+  void play(Ts... x) override { /* ignore - see play_complex */
+  }
+
+  void stop() override { this->cancel_timeout(""); }
+};
+
+template<typename... Ts> class LambdaAction : public Action<Ts...> {
+ public:
+  explicit LambdaAction(std::function<void(Ts...)> &&f) : f_(std::move(f)) {}
+
+  void play(Ts... x) override { this->f_(x...); }
+
+ protected:
+  std::function<void(Ts...)> f_;
+};
+
+template<typename... Ts> class IfAction : public Action<Ts...> {
+ public:
+  explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}
+
+  void add_then(const std::vector<Action<Ts...> *> &actions) {
+    this->then_.add_actions(actions);
+    this->then_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
+  }
+
+  void add_else(const std::vector<Action<Ts...> *> &actions) {
+    this->else_.add_actions(actions);
+    this->else_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
+  }
+
+  void play_complex(Ts... x) override {
+    this->num_running_++;
+    bool res = this->condition_->check(x...);
+    if (res) {
+      if (this->then_.empty()) {
+        this->play_next_(x...);
+      } else if (this->num_running_ > 0) {
+        this->then_.play(x...);
+      }
+    } else {
+      if (this->else_.empty()) {
+        this->play_next_(x...);
+      } else if (this->num_running_ > 0) {
+        this->else_.play(x...);
+      }
+    }
+  }
+
+  void play(Ts... x) override { /* ignore - see play_complex */
+  }
+
+  void stop() override {
+    this->then_.stop();
+    this->else_.stop();
+  }
+
+ protected:
+  Condition<Ts...> *condition_;
+  ActionList<Ts...> then_;
+  ActionList<Ts...> else_;
+};
+
+template<typename... Ts> class WhileAction : public Action<Ts...> {
+ public:
+  WhileAction(Condition<Ts...> *condition) : condition_(condition) {}
+
+  void add_then(const std::vector<Action<Ts...> *> &actions) {
+    this->then_.add_actions(actions);
+    this->then_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
+      if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) {
+        // play again
+        if (this->num_running_ > 0) {
+          this->then_.play_tuple(this->var_);
+        }
+      } else {
+        // condition false, play next
+        this->play_next_tuple_(this->var_);
+      }
+    }));
+  }
+
+  void play_complex(Ts... x) override {
+    this->num_running_++;
+    // Store loop parameters
+    this->var_ = std::make_tuple(x...);
+    // Initial condition check
+    if (!this->condition_->check_tuple(this->var_)) {
+      // If new condition check failed, stop loop if running
+      this->then_.stop();
+      this->play_next_tuple_(this->var_);
+      return;
+    }
+
+    if (this->num_running_ > 0) {
+      this->then_.play_tuple(this->var_);
+    }
+  }
+
+  void play(Ts... x) override { /* ignore - see play_complex */
+  }
+
+  void stop() override { this->then_.stop(); }
+
+ protected:
+  Condition<Ts...> *condition_;
+  ActionList<Ts...> then_;
+  std::tuple<Ts...> var_{};
+};
+
+template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Component {
+ public:
+  WaitUntilAction(Condition<Ts...> *condition) : condition_(condition) {}
+
+  void play_complex(Ts... x) override {
+    this->num_running_++;
+    // Check if we can continue immediately.
+    if (this->condition_->check(x...)) {
+      if (this->num_running_ > 0) {
+        this->play_next_(x...);
+      }
+      return;
+    }
+    this->var_ = std::make_tuple(x...);
+    this->loop();
+  }
+
+  void loop() override {
+    if (this->num_running_ == 0)
+      return;
+
+    if (!this->condition_->check_tuple(this->var_)) {
+      return;
+    }
+
+    this->play_next_tuple_(this->var_);
+  }
+
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+  void play(Ts... x) override { /* ignore - see play_complex */
+  }
+
+ protected:
+  Condition<Ts...> *condition_;
+  std::tuple<Ts...> var_{};
+};
+
+template<typename... Ts> class UpdateComponentAction : public Action<Ts...> {
+ public:
+  UpdateComponentAction(PollingComponent *component) : component_(component) {}
+
+  void play(Ts... x) override { this->component_->update(); }
+
+ protected:
+  PollingComponent *component_;
+};
+
+}  // namespace esphome

+ 251 - 0
livingroom/src/esphome/core/color.h

@@ -0,0 +1,251 @@
+#pragma once
+
+#include "component.h"
+#include "helpers.h"
+
+namespace esphome {
+
+inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; }
+inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 255) { return (max_value * i / scale); }
+
+struct Color {
+  union {
+    struct {
+      union {
+        uint8_t r;
+        uint8_t red;
+      };
+      union {
+        uint8_t g;
+        uint8_t green;
+      };
+      union {
+        uint8_t b;
+        uint8_t blue;
+      };
+      union {
+        uint8_t w;
+        uint8_t white;
+      };
+    };
+    uint8_t raw[4];
+    uint32_t raw_32;
+  };
+  enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
+  enum ColorBitness : uint8_t { COLOR_BITNESS_888 = 0, COLOR_BITNESS_565 = 1, COLOR_BITNESS_332 = 2 };
+  inline Color() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {}  // NOLINT
+  inline Color(float red, float green, float blue) ALWAYS_INLINE : r(uint8_t(red * 255)),
+                                                                   g(uint8_t(green * 255)),
+                                                                   b(uint8_t(blue * 255)),
+                                                                   w(0) {}
+  inline Color(float red, float green, float blue, float white) ALWAYS_INLINE : r(uint8_t(red * 255)),
+                                                                                g(uint8_t(green * 255)),
+                                                                                b(uint8_t(blue * 255)),
+                                                                                w(uint8_t(white * 255)) {}
+  inline Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF),
+                                                   g((colorcode >> 8) & 0xFF),
+                                                   b((colorcode >> 0) & 0xFF),
+                                                   w((colorcode >> 24) & 0xFF) {}
+  inline Color(uint32_t colorcode, ColorOrder color_order, ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888,
+               bool right_bit_aligned = true) {
+    uint8_t first_color, second_color, third_color;
+    uint8_t first_bits = 0;
+    uint8_t second_bits = 0;
+    uint8_t third_bits = 0;
+
+    switch (color_bitness) {
+      case COLOR_BITNESS_888:
+        first_bits = 8;
+        second_bits = 8;
+        third_bits = 8;
+        break;
+      case COLOR_BITNESS_565:
+        first_bits = 5;
+        second_bits = 6;
+        third_bits = 5;
+        break;
+      case COLOR_BITNESS_332:
+        first_bits = 3;
+        second_bits = 3;
+        third_bits = 2;
+        break;
+    }
+
+    first_color = right_bit_aligned ? esp_scale(((colorcode >> (second_bits + third_bits)) & ((1 << first_bits) - 1)),
+                                                ((1 << first_bits) - 1))
+                                    : esp_scale(((colorcode >> 16) & 0xFF), (1 << first_bits) - 1);
+
+    second_color = right_bit_aligned
+                       ? esp_scale(((colorcode >> third_bits) & ((1 << second_bits) - 1)), ((1 << second_bits) - 1))
+                       : esp_scale(((colorcode >> 8) & 0xFF), ((1 << second_bits) - 1));
+
+    third_color = (right_bit_aligned ? esp_scale(((colorcode >> 0) & 0xFF), ((1 << third_bits) - 1))
+                                     : esp_scale(((colorcode >> 0) & 0xFF), (1 << third_bits) - 1));
+
+    switch (color_order) {
+      case COLOR_ORDER_RGB:
+        this->r = first_color;
+        this->g = second_color;
+        this->b = third_color;
+        break;
+      case COLOR_ORDER_BGR:
+        this->b = first_color;
+        this->g = second_color;
+        this->r = third_color;
+        break;
+      case COLOR_ORDER_GRB:
+        this->g = first_color;
+        this->r = second_color;
+        this->b = third_color;
+        break;
+    }
+  }
+  inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; }
+  inline Color &operator=(const Color &rhs) ALWAYS_INLINE {
+    this->r = rhs.r;
+    this->g = rhs.g;
+    this->b = rhs.b;
+    this->w = rhs.w;
+    return *this;
+  }
+  inline Color &operator=(uint32_t colorcode) ALWAYS_INLINE {
+    this->w = (colorcode >> 24) & 0xFF;
+    this->r = (colorcode >> 16) & 0xFF;
+    this->g = (colorcode >> 8) & 0xFF;
+    this->b = (colorcode >> 0) & 0xFF;
+    return *this;
+  }
+  inline uint8_t &operator[](uint8_t x) ALWAYS_INLINE { return this->raw[x]; }
+  inline Color operator*(uint8_t scale) const ALWAYS_INLINE {
+    return Color(esp_scale8(this->red, scale), esp_scale8(this->green, scale), esp_scale8(this->blue, scale),
+                 esp_scale8(this->white, scale));
+  }
+  inline Color &operator*=(uint8_t scale) ALWAYS_INLINE {
+    this->red = esp_scale8(this->red, scale);
+    this->green = esp_scale8(this->green, scale);
+    this->blue = esp_scale8(this->blue, scale);
+    this->white = esp_scale8(this->white, scale);
+    return *this;
+  }
+  inline Color operator*(const Color &scale) const ALWAYS_INLINE {
+    return Color(esp_scale8(this->red, scale.red), esp_scale8(this->green, scale.green),
+                 esp_scale8(this->blue, scale.blue), esp_scale8(this->white, scale.white));
+  }
+  inline Color &operator*=(const Color &scale) ALWAYS_INLINE {
+    this->red = esp_scale8(this->red, scale.red);
+    this->green = esp_scale8(this->green, scale.green);
+    this->blue = esp_scale8(this->blue, scale.blue);
+    this->white = esp_scale8(this->white, scale.white);
+    return *this;
+  }
+  inline Color operator+(const Color &add) const ALWAYS_INLINE {
+    Color ret;
+    if (uint8_t(add.r + this->r) < this->r)
+      ret.r = 255;
+    else
+      ret.r = this->r + add.r;
+    if (uint8_t(add.g + this->g) < this->g)
+      ret.g = 255;
+    else
+      ret.g = this->g + add.g;
+    if (uint8_t(add.b + this->b) < this->b)
+      ret.b = 255;
+    else
+      ret.b = this->b + add.b;
+    if (uint8_t(add.w + this->w) < this->w)
+      ret.w = 255;
+    else
+      ret.w = this->w + add.w;
+    return ret;
+  }
+  inline Color &operator+=(const Color &add) ALWAYS_INLINE { return *this = (*this) + add; }
+  inline Color operator+(uint8_t add) const ALWAYS_INLINE { return (*this) + Color(add, add, add, add); }
+  inline Color &operator+=(uint8_t add) ALWAYS_INLINE { return *this = (*this) + add; }
+  inline Color operator-(const Color &subtract) const ALWAYS_INLINE {
+    Color ret;
+    if (subtract.r > this->r)
+      ret.r = 0;
+    else
+      ret.r = this->r - subtract.r;
+    if (subtract.g > this->g)
+      ret.g = 0;
+    else
+      ret.g = this->g - subtract.g;
+    if (subtract.b > this->b)
+      ret.b = 0;
+    else
+      ret.b = this->b - subtract.b;
+    if (subtract.w > this->w)
+      ret.w = 0;
+    else
+      ret.w = this->w - subtract.w;
+    return ret;
+  }
+  inline Color &operator-=(const Color &subtract) ALWAYS_INLINE { return *this = (*this) - subtract; }
+  inline Color operator-(uint8_t subtract) const ALWAYS_INLINE {
+    return (*this) - Color(subtract, subtract, subtract, subtract);
+  }
+  inline Color &operator-=(uint8_t subtract) ALWAYS_INLINE { return *this = (*this) - subtract; }
+  static Color random_color() {
+    float r = float(random_uint32()) / float(UINT32_MAX);
+    float g = float(random_uint32()) / float(UINT32_MAX);
+    float b = float(random_uint32()) / float(UINT32_MAX);
+    float w = float(random_uint32()) / float(UINT32_MAX);
+    return Color(r, g, b, w);
+  }
+  Color fade_to_white(uint8_t amnt) { return Color(1, 1, 1, 1) - (*this * amnt); }
+  Color fade_to_black(uint8_t amnt) { return *this * amnt; }
+  Color lighten(uint8_t delta) { return *this + delta; }
+  Color darken(uint8_t delta) { return *this - delta; }
+  uint8_t to_332(ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) const {
+    uint16_t red_color, green_color, blue_color;
+
+    red_color = esp_scale8(this->red, ((1 << 3) - 1));
+    green_color = esp_scale8(this->green, ((1 << 3) - 1));
+    blue_color = esp_scale8(this->blue, (1 << 2) - 1);
+
+    switch (color_order) {
+      case COLOR_ORDER_RGB:
+        return red_color << 5 | green_color << 2 | blue_color;
+      case COLOR_ORDER_BGR:
+        return blue_color << 6 | green_color << 3 | red_color;
+      case COLOR_ORDER_GRB:
+        return green_color << 5 | red_color << 2 | blue_color;
+    }
+    return 0;
+  }
+  uint16_t to_565(ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) const {
+    uint16_t red_color, green_color, blue_color;
+
+    red_color = esp_scale8(this->red, ((1 << 5) - 1));
+    green_color = esp_scale8(this->green, ((1 << 6) - 1));
+    blue_color = esp_scale8(this->blue, (1 << 5) - 1);
+
+    switch (color_order) {
+      case COLOR_ORDER_RGB:
+        return red_color << 11 | green_color << 5 | blue_color;
+      case COLOR_ORDER_BGR:
+        return blue_color << 11 | green_color << 5 | red_color;
+      case COLOR_ORDER_GRB:
+        return green_color << 10 | red_color << 5 | blue_color;
+    }
+    return 0;
+  }
+  uint32_t to_rgb_565() const {
+    uint32_t color565 =
+        (esp_scale8(this->red, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->blue, 31) << 0);
+    return color565;
+  }
+  uint32_t to_bgr_565() const {
+    uint32_t color565 =
+        (esp_scale8(this->blue, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->red, 31) << 0);
+    return color565;
+  }
+  uint32_t to_grayscale4() const {
+    uint32_t gs4 = esp_scale8(this->white, 15);
+    return gs4;
+  }
+};
+static const Color COLOR_BLACK(0, 0, 0);
+static const Color COLOR_WHITE(1, 1, 1);
+};  // namespace esphome

+ 186 - 0
livingroom/src/esphome/core/component.cpp

@@ -0,0 +1,186 @@
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/esphal.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+
+static const char *TAG = "component";
+
+namespace setup_priority {
+
+const float BUS = 1000.0f;
+const float IO = 900.0f;
+const float HARDWARE = 800.0f;
+const float DATA = 600.0f;
+const float PROCESSOR = 400.0;
+const float WIFI = 250.0f;
+const float AFTER_WIFI = 200.0f;
+const float AFTER_CONNECTION = 100.0f;
+const float LATE = -100.0f;
+
+}  // namespace setup_priority
+
+const uint32_t COMPONENT_STATE_MASK = 0xFF;
+const uint32_t COMPONENT_STATE_CONSTRUCTION = 0x00;
+const uint32_t COMPONENT_STATE_SETUP = 0x01;
+const uint32_t COMPONENT_STATE_LOOP = 0x02;
+const uint32_t COMPONENT_STATE_FAILED = 0x03;
+const uint32_t STATUS_LED_MASK = 0xFF00;
+const uint32_t STATUS_LED_OK = 0x0000;
+const uint32_t STATUS_LED_WARNING = 0x0100;
+const uint32_t STATUS_LED_ERROR = 0x0200;
+
+uint32_t global_state = 0;
+
+float Component::get_loop_priority() const { return 0.0f; }
+
+float Component::get_setup_priority() const { return setup_priority::DATA; }
+
+void Component::setup() {}
+
+void Component::loop() {}
+
+void Component::set_interval(const std::string &name, uint32_t interval, std::function<void()> &&f) {  // NOLINT
+  App.scheduler.set_interval(this, name, interval, std::move(f));
+}
+
+bool Component::cancel_interval(const std::string &name) {  // NOLINT
+  return App.scheduler.cancel_interval(this, name);
+}
+
+void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) {  // NOLINT
+  return App.scheduler.set_timeout(this, name, timeout, std::move(f));
+}
+
+bool Component::cancel_timeout(const std::string &name) {  // NOLINT
+  return App.scheduler.cancel_timeout(this, name);
+}
+
+void Component::call_loop() { this->loop(); }
+
+void Component::call_setup() { this->setup(); }
+uint32_t Component::get_component_state() const { return this->component_state_; }
+void Component::call() {
+  uint32_t state = this->component_state_ & COMPONENT_STATE_MASK;
+  switch (state) {
+    case COMPONENT_STATE_CONSTRUCTION:
+      // State Construction: Call setup and set state to setup
+      this->component_state_ &= ~COMPONENT_STATE_MASK;
+      this->component_state_ |= COMPONENT_STATE_SETUP;
+      this->call_setup();
+      break;
+    case COMPONENT_STATE_SETUP:
+      // State setup: Call first loop and set state to loop
+      this->component_state_ &= ~COMPONENT_STATE_MASK;
+      this->component_state_ |= COMPONENT_STATE_LOOP;
+      this->call_loop();
+      break;
+    case COMPONENT_STATE_LOOP:
+      // State loop: Call loop
+      this->call_loop();
+      break;
+    case COMPONENT_STATE_FAILED:
+      // State failed: Do nothing
+      break;
+    default:
+      break;
+  }
+}
+void Component::mark_failed() {
+  ESP_LOGE(TAG, "Component was marked as failed.");
+  this->component_state_ &= ~COMPONENT_STATE_MASK;
+  this->component_state_ |= COMPONENT_STATE_FAILED;
+  this->status_set_error();
+}
+void Component::defer(std::function<void()> &&f) {  // NOLINT
+  App.scheduler.set_timeout(this, "", 0, std::move(f));
+}
+bool Component::cancel_defer(const std::string &name) {  // NOLINT
+  return App.scheduler.cancel_timeout(this, name);
+}
+void Component::defer(const std::string &name, std::function<void()> &&f) {  // NOLINT
+  App.scheduler.set_timeout(this, name, 0, std::move(f));
+}
+void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) {  // NOLINT
+  App.scheduler.set_timeout(this, "", timeout, std::move(f));
+}
+void Component::set_interval(uint32_t interval, std::function<void()> &&f) {  // NOLINT
+  App.scheduler.set_interval(this, "", interval, std::move(f));
+}
+bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
+bool Component::can_proceed() { return true; }
+bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; }
+bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; }
+void Component::status_set_warning() {
+  this->component_state_ |= STATUS_LED_WARNING;
+  App.app_state_ |= STATUS_LED_WARNING;
+}
+void Component::status_set_error() {
+  this->component_state_ |= STATUS_LED_ERROR;
+  App.app_state_ |= STATUS_LED_ERROR;
+}
+void Component::status_clear_warning() { this->component_state_ &= ~STATUS_LED_WARNING; }
+void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; }
+void Component::status_momentary_warning(const std::string &name, uint32_t length) {
+  this->status_set_warning();
+  this->set_timeout(name, length, [this]() { this->status_clear_warning(); });
+}
+void Component::status_momentary_error(const std::string &name, uint32_t length) {
+  this->status_set_error();
+  this->set_timeout(name, length, [this]() { this->status_clear_error(); });
+}
+void Component::dump_config() {}
+float Component::get_actual_setup_priority() const {
+  if (isnan(this->setup_priority_override_))
+    return this->get_setup_priority();
+  return this->setup_priority_override_;
+}
+void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; }
+
+bool Component::has_overridden_loop() const {
+#ifdef CLANG_TIDY
+  bool loop_overridden = true;
+  bool call_loop_overridden = true;
+#else
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpmf-conversions"
+  bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop);
+  bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop);
+#pragma GCC diagnostic pop
+#endif
+  return loop_overridden || call_loop_overridden;
+}
+
+PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {}
+
+void PollingComponent::call_setup() {
+  // Let the polling component subclass setup their HW.
+  this->setup();
+
+  // Register interval.
+  this->set_interval("update", this->get_update_interval(), [this]() { this->update(); });
+}
+
+uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
+void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
+
+const std::string &Nameable::get_name() const { return this->name_; }
+void Nameable::set_name(const std::string &name) {
+  this->name_ = name;
+  this->calc_object_id_();
+}
+Nameable::Nameable(const std::string &name) : name_(name) { this->calc_object_id_(); }
+
+const std::string &Nameable::get_object_id() { return this->object_id_; }
+bool Nameable::is_internal() const { return this->internal_; }
+void Nameable::set_internal(bool internal) { this->internal_ = internal; }
+void Nameable::calc_object_id_() {
+  this->object_id_ = sanitize_string_allowlist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_ALLOWLIST);
+  // FNV-1 hash
+  this->object_id_hash_ = fnv1_hash(this->object_id_);
+}
+uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; }
+
+}  // namespace esphome

+ 268 - 0
livingroom/src/esphome/core/component.h

@@ -0,0 +1,268 @@
+#pragma once
+
+#include <string>
+#include <functional>
+#include "Arduino.h"
+
+#include "esphome/core/optional.h"
+
+namespace esphome {
+
+/** Default setup priorities for components of different types.
+ *
+ * Components should return one of these setup priorities in get_setup_priority.
+ */
+namespace setup_priority {
+
+/// For communication buses like i2c/spi
+extern const float BUS;
+/// For components that represent GPIO pins like PCF8573
+extern const float IO;
+/// For components that deal with hardware and are very important like GPIO switch
+extern const float HARDWARE;
+/// For components that import data from directly connected sensors like DHT.
+extern const float DATA;
+/// Alias for DATA (here for compatability reasons)
+extern const float HARDWARE_LATE;
+/// For components that use data from sensors like displays
+extern const float PROCESSOR;
+extern const float WIFI;
+/// For components that should be initialized after WiFi is connected.
+extern const float AFTER_WIFI;
+/// For components that should be initialized after a data connection (API/MQTT) is connected.
+extern const float AFTER_CONNECTION;
+/// For components that should be initialized at the very end of the setup process.
+extern const float LATE;
+
+}  // namespace setup_priority
+
+#define LOG_UPDATE_INTERVAL(this) \
+  if (this->get_update_interval() < 100) { \
+    ESP_LOGCONFIG(TAG, "  Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \
+  } else { \
+    ESP_LOGCONFIG(TAG, "  Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \
+  }
+
+extern const uint32_t COMPONENT_STATE_MASK;
+extern const uint32_t COMPONENT_STATE_CONSTRUCTION;
+extern const uint32_t COMPONENT_STATE_SETUP;
+extern const uint32_t COMPONENT_STATE_LOOP;
+extern const uint32_t COMPONENT_STATE_FAILED;
+extern const uint32_t STATUS_LED_MASK;
+extern const uint32_t STATUS_LED_OK;
+extern const uint32_t STATUS_LED_WARNING;
+extern const uint32_t STATUS_LED_ERROR;
+
+class Component {
+ public:
+  /** Where the component's initialization should happen.
+   *
+   * Analogous to Arduino's setup(). This method is guaranteed to only be called once.
+   * Defaults to doing nothing.
+   */
+  virtual void setup();
+
+  /** This method will be called repeatedly.
+   *
+   * Analogous to Arduino's loop(). setup() is guaranteed to be called before this.
+   * Defaults to doing nothing.
+   */
+  virtual void loop();
+
+  virtual void dump_config();
+
+  /** priority of setup(). higher -> executed earlier
+   *
+   * Defaults to 0.
+   *
+   * @return The setup priority of this component
+   */
+  virtual float get_setup_priority() const;
+
+  float get_actual_setup_priority() const;
+
+  void set_setup_priority(float priority);
+
+  /** priority of loop(). higher -> executed earlier
+   *
+   * Defaults to 0.
+   *
+   * @return The loop priority of this component
+   */
+  virtual float get_loop_priority() const;
+
+  void call();
+
+  virtual void on_shutdown() {}
+  virtual void on_safe_shutdown() {}
+
+  uint32_t get_component_state() const;
+
+  /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called.
+   *
+   * This might be useful if a component wants to indicate that a connection to its peripheral failed.
+   * For example, i2c based components can check if the remote device is responding and otherwise
+   * mark the component as failed. Eventually this will also enable smart status LEDs.
+   */
+  virtual void mark_failed();
+
+  bool is_failed();
+
+  virtual bool can_proceed();
+
+  bool status_has_warning();
+
+  bool status_has_error();
+
+  void status_set_warning();
+
+  void status_set_error();
+
+  void status_clear_warning();
+
+  void status_clear_error();
+
+  void status_momentary_warning(const std::string &name, uint32_t length = 5000);
+
+  void status_momentary_error(const std::string &name, uint32_t length = 5000);
+
+  bool has_overridden_loop() const;
+
+ protected:
+  virtual void call_loop();
+  virtual void call_setup();
+  /** Set an interval function with a unique name. Empty name means no cancelling possible.
+   *
+   * This will call f every interval ms. Can be cancelled via CancelInterval().
+   * Similar to javascript's setInterval().
+   *
+   * IMPORTANT: Do not rely on this having correct timing. This is only called from
+   * loop() and therefore can be significantly delay. If you need exact timing please
+   * use hardware timers.
+   *
+   * @param name The identifier for this interval function.
+   * @param interval The interval in ms.
+   * @param f The function (or lambda) that should be called
+   *
+   * @see cancel_interval()
+   */
+  void set_interval(const std::string &name, uint32_t interval, std::function<void()> &&f);  // NOLINT
+
+  void set_interval(uint32_t interval, std::function<void()> &&f);  // NOLINT
+
+  /** Cancel an interval function.
+   *
+   * @param name The identifier for this interval function.
+   * @return Whether an interval functions was deleted.
+   */
+  bool cancel_interval(const std::string &name);  // NOLINT
+
+  void set_timeout(uint32_t timeout, std::function<void()> &&f);  // NOLINT
+
+  /** Set a timeout function with a unique name.
+   *
+   * Similar to javascript's setTimeout(). Empty name means no cancelling possible.
+   *
+   * IMPORTANT: Do not rely on this having correct timing. This is only called from
+   * loop() and therefore can be significantly delay. If you need exact timing please
+   * use hardware timers.
+   *
+   * @param name The identifier for this timeout function.
+   * @param timeout The timeout in ms.
+   * @param f The function (or lambda) that should be called
+   *
+   * @see cancel_timeout()
+   */
+  void set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f);  // NOLINT
+
+  /** Cancel a timeout function.
+   *
+   * @param name The identifier for this timeout function.
+   * @return Whether a timeout functions was deleted.
+   */
+  bool cancel_timeout(const std::string &name);  // NOLINT
+
+  /** Defer a callback to the next loop() call.
+   *
+   * If name is specified and a defer() object with the same name exists, the old one is first removed.
+   *
+   * @param name The name of the defer function.
+   * @param f The callback.
+   */
+  void defer(const std::string &name, std::function<void()> &&f);  // NOLINT
+
+  /// Defer a callback to the next loop() call.
+  void defer(std::function<void()> &&f);  // NOLINT
+
+  /// Cancel a defer callback using the specified name, name must not be empty.
+  bool cancel_defer(const std::string &name);  // NOLINT
+
+  uint32_t component_state_{0x0000};  ///< State of this component.
+  float setup_priority_override_{NAN};
+};
+
+/** This class simplifies creating components that periodically check a state.
+ *
+ * You basically just need to implement the update() function, it will be called every update_interval ms
+ * after startup. Note that this class cannot guarantee a correct timing, as it's not using timers, just
+ * a software polling feature with set_interval() from Component.
+ */
+class PollingComponent : public Component {
+ public:
+  PollingComponent() : PollingComponent(0) {}
+
+  /** Initialize this polling component with the given update interval in ms.
+   *
+   * @param update_interval The update interval in ms.
+   */
+  explicit PollingComponent(uint32_t update_interval);
+
+  /** Manually set the update interval in ms for this polling object.
+   *
+   * Override this if you want to do some validation for the update interval.
+   *
+   * @param update_interval The update interval in ms.
+   */
+  virtual void set_update_interval(uint32_t update_interval);
+
+  // ========== OVERRIDE METHODS ==========
+  // (You'll only need this when creating your own custom sensor)
+  virtual void update() = 0;
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  void call_setup() override;
+
+  /// Get the update interval in ms of this sensor
+  virtual uint32_t get_update_interval() const;
+
+ protected:
+  uint32_t update_interval_;
+};
+
+/// Helper class that enables naming of objects so that it doesn't have to be re-implement every time.
+class Nameable {
+ public:
+  Nameable() : Nameable("") {}
+  explicit Nameable(const std::string &name);
+  const std::string &get_name() const;
+  void set_name(const std::string &name);
+  /// Get the sanitized name of this nameable as an ID. Caching it internally.
+  const std::string &get_object_id();
+  uint32_t get_object_id_hash();
+
+  bool is_internal() const;
+  void set_internal(bool internal);
+
+ protected:
+  virtual uint32_t hash_base() = 0;
+
+  void calc_object_id_();
+
+  std::string name_;
+  std::string object_id_;
+  uint32_t object_id_hash_;
+  bool internal_{false};
+};
+
+}  // namespace esphome

+ 58 - 0
livingroom/src/esphome/core/controller.cpp

@@ -0,0 +1,58 @@
+#include "controller.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+
+void Controller::setup_controller() {
+#ifdef USE_BINARY_SENSOR
+  for (auto *obj : App.get_binary_sensors()) {
+    if (!obj->is_internal())
+      obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); });
+  }
+#endif
+#ifdef USE_FAN
+  for (auto *obj : App.get_fans()) {
+    if (!obj->is_internal())
+      obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); });
+  }
+#endif
+#ifdef USE_LIGHT
+  for (auto *obj : App.get_lights()) {
+    if (!obj->is_internal())
+      obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); });
+  }
+#endif
+#ifdef USE_SENSOR
+  for (auto *obj : App.get_sensors()) {
+    if (!obj->is_internal())
+      obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); });
+  }
+#endif
+#ifdef USE_SWITCH
+  for (auto *obj : App.get_switches()) {
+    if (!obj->is_internal())
+      obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); });
+  }
+#endif
+#ifdef USE_COVER
+  for (auto *obj : App.get_covers()) {
+    if (!obj->is_internal())
+      obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); });
+  }
+#endif
+#ifdef USE_TEXT_SENSOR
+  for (auto *obj : App.get_text_sensors()) {
+    if (!obj->is_internal())
+      obj->add_on_state_callback([this, obj](std::string state) { this->on_text_sensor_update(obj, state); });
+  }
+#endif
+#ifdef USE_CLIMATE
+  for (auto *obj : App.get_climates()) {
+    if (!obj->is_internal())
+      obj->add_on_state_callback([this, obj]() { this->on_climate_update(obj); });
+  }
+#endif
+}
+
+}  // namespace esphome

+ 60 - 0
livingroom/src/esphome/core/controller.h

@@ -0,0 +1,60 @@
+#pragma once
+
+#include "esphome/core/defines.h"
+#ifdef USE_BINARY_SENSOR
+#include "esphome/components/binary_sensor/binary_sensor.h"
+#endif
+#ifdef USE_FAN
+#include "esphome/components/fan/fan_state.h"
+#endif
+#ifdef USE_LIGHT
+#include "esphome/components/light/light_state.h"
+#endif
+#ifdef USE_COVER
+#include "esphome/components/cover/cover.h"
+#endif
+#ifdef USE_SENSOR
+#include "esphome/components/sensor/sensor.h"
+#endif
+#ifdef USE_TEXT_SENSOR
+#include "esphome/components/text_sensor/text_sensor.h"
+#endif
+#ifdef USE_SWITCH
+#include "esphome/components/switch/switch.h"
+#endif
+#ifdef USE_CLIMATE
+#include "esphome/components/climate/climate.h"
+#endif
+
+namespace esphome {
+
+class Controller {
+ public:
+  void setup_controller();
+#ifdef USE_BINARY_SENSOR
+  virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){};
+#endif
+#ifdef USE_FAN
+  virtual void on_fan_update(fan::FanState *obj){};
+#endif
+#ifdef USE_LIGHT
+  virtual void on_light_update(light::LightState *obj){};
+#endif
+#ifdef USE_SENSOR
+  virtual void on_sensor_update(sensor::Sensor *obj, float state){};
+#endif
+#ifdef USE_SWITCH
+  virtual void on_switch_update(switch_::Switch *obj, bool state){};
+#endif
+#ifdef USE_COVER
+  virtual void on_cover_update(cover::Cover *obj){};
+#endif
+#ifdef USE_TEXT_SENSOR
+  virtual void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state){};
+#endif
+#ifdef USE_CLIMATE
+  virtual void on_climate_update(climate::Climate *obj){};
+#endif
+};
+
+}  // namespace esphome

+ 10 - 0
livingroom/src/esphome/core/defines.h

@@ -0,0 +1,10 @@
+#pragma once
+#define USE_API
+#define USE_CAPTIVE_PORTAL
+#define USE_HOMEASSISTANT_TIME
+#define USE_JSON
+#define USE_LOGGER
+#define USE_TEXT_SENSOR
+#define USE_TIME
+#define USE_WIFI
+#define WEBSERVER_PORT 80

+ 317 - 0
livingroom/src/esphome/core/esphal.cpp

@@ -0,0 +1,317 @@
+#include "esphome/core/esphal.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP8266
+extern "C" {
+typedef struct {        // NOLINT
+  void *interruptInfo;  // NOLINT
+  void *functionInfo;   // NOLINT
+} ArgStructure;
+
+void ICACHE_RAM_ATTR __attachInterruptArg(uint8_t pin, void (*)(void *), void *fp,  // NOLINT
+                                          int mode);
+void ICACHE_RAM_ATTR __detachInterrupt(uint8_t pin);  // NOLINT
+};
+#endif
+
+namespace esphome {
+
+static const char *TAG = "esphal";
+
+GPIOPin::GPIOPin(uint8_t pin, uint8_t mode, bool inverted)
+    : pin_(pin),
+      mode_(mode),
+      inverted_(inverted),
+#ifdef ARDUINO_ARCH_ESP8266
+      gpio_read_(pin < 16 ? &GPI : &GP16I),
+      gpio_mask_(pin < 16 ? (1UL << pin) : 1)
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+          gpio_set_(pin < 32 ? &GPIO.out_w1ts : &GPIO.out1_w1ts.val),
+      gpio_clear_(pin < 32 ? &GPIO.out_w1tc : &GPIO.out1_w1tc.val),
+      gpio_read_(pin < 32 ? &GPIO.in : &GPIO.in1.val),
+      gpio_mask_(pin < 32 ? (1UL << pin) : (1UL << (pin - 32)))
+#endif
+{
+}
+
+const char *GPIOPin::get_pin_mode_name() const {
+  const char *mode_s;
+  switch (this->mode_) {
+    case INPUT:
+      mode_s = "INPUT";
+      break;
+    case OUTPUT:
+      mode_s = "OUTPUT";
+      break;
+    case INPUT_PULLUP:
+      mode_s = "INPUT_PULLUP";
+      break;
+    case OUTPUT_OPEN_DRAIN:
+      mode_s = "OUTPUT_OPEN_DRAIN";
+      break;
+    case SPECIAL:
+      mode_s = "SPECIAL";
+      break;
+    case FUNCTION_1:
+      mode_s = "FUNCTION_1";
+      break;
+    case FUNCTION_2:
+      mode_s = "FUNCTION_2";
+      break;
+    case FUNCTION_3:
+      mode_s = "FUNCTION_3";
+      break;
+    case FUNCTION_4:
+      mode_s = "FUNCTION_4";
+      break;
+
+#ifdef ARDUINO_ARCH_ESP32
+    case PULLUP:
+      mode_s = "PULLUP";
+      break;
+    case PULLDOWN:
+      mode_s = "PULLDOWN";
+      break;
+    case INPUT_PULLDOWN:
+      mode_s = "INPUT_PULLDOWN";
+      break;
+    case OPEN_DRAIN:
+      mode_s = "OPEN_DRAIN";
+      break;
+    case FUNCTION_5:
+      mode_s = "FUNCTION_5";
+      break;
+    case FUNCTION_6:
+      mode_s = "FUNCTION_6";
+      break;
+    case ANALOG:
+      mode_s = "ANALOG";
+      break;
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+    case FUNCTION_0:
+      mode_s = "FUNCTION_0";
+      break;
+    case WAKEUP_PULLUP:
+      mode_s = "WAKEUP_PULLUP";
+      break;
+    case WAKEUP_PULLDOWN:
+      mode_s = "WAKEUP_PULLDOWN";
+      break;
+    case INPUT_PULLDOWN_16:
+      mode_s = "INPUT_PULLDOWN_16";
+      break;
+#endif
+
+    default:
+      mode_s = "UNKNOWN";
+      break;
+  }
+
+  return mode_s;
+}
+
+unsigned char GPIOPin::get_pin() const { return this->pin_; }
+unsigned char GPIOPin::get_mode() const { return this->mode_; }
+
+bool GPIOPin::is_inverted() const { return this->inverted_; }
+void GPIOPin::setup() { this->pin_mode(this->mode_); }
+bool ICACHE_RAM_ATTR HOT GPIOPin::digital_read() {
+  return bool((*this->gpio_read_) & this->gpio_mask_) != this->inverted_;
+}
+bool ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_read() {
+  return bool((*this->gpio_read_) & this->gpio_mask_) != this->inverted_;
+}
+void ICACHE_RAM_ATTR HOT GPIOPin::digital_write(bool value) {
+#ifdef ARDUINO_ARCH_ESP8266
+  if (this->pin_ != 16) {
+    if (value != this->inverted_) {
+      GPOS = this->gpio_mask_;
+    } else {
+      GPOC = this->gpio_mask_;
+    }
+  } else {
+    if (value != this->inverted_) {
+      GP16O |= 1;
+    } else {
+      GP16O &= ~1;
+    }
+  }
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+  if (value != this->inverted_) {
+    (*this->gpio_set_) = this->gpio_mask_;
+  } else {
+    (*this->gpio_clear_) = this->gpio_mask_;
+  }
+#endif
+}
+void ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_write(bool value) {
+#ifdef ARDUINO_ARCH_ESP8266
+  if (this->pin_ != 16) {
+    if (value != this->inverted_) {
+      GPOS = this->gpio_mask_;
+    } else {
+      GPOC = this->gpio_mask_;
+    }
+  } else {
+    if (value != this->inverted_) {
+      GP16O |= 1;
+    } else {
+      GP16O &= ~1;
+    }
+  }
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+  if (value != this->inverted_) {
+    (*this->gpio_set_) = this->gpio_mask_;
+  } else {
+    (*this->gpio_clear_) = this->gpio_mask_;
+  }
+#endif
+}
+ISRInternalGPIOPin::ISRInternalGPIOPin(uint8_t pin,
+#ifdef ARDUINO_ARCH_ESP32
+                                       volatile uint32_t *gpio_clear, volatile uint32_t *gpio_set,
+#endif
+                                       volatile uint32_t *gpio_read, uint32_t gpio_mask, bool inverted)
+    : pin_(pin),
+      inverted_(inverted),
+      gpio_read_(gpio_read),
+      gpio_mask_(gpio_mask)
+#ifdef ARDUINO_ARCH_ESP32
+      ,
+      gpio_clear_(gpio_clear),
+      gpio_set_(gpio_set)
+#endif
+{
+}
+void ICACHE_RAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
+#ifdef ARDUINO_ARCH_ESP8266
+  GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, this->gpio_mask_);
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+  if (this->pin_ < 32) {
+    GPIO.status_w1tc = this->gpio_mask_;
+  } else {
+    GPIO.status1_w1tc.intr_st = this->gpio_mask_;
+  }
+#endif
+}
+
+void ICACHE_RAM_ATTR HOT GPIOPin::pin_mode(uint8_t mode) {
+#ifdef ARDUINO_ARCH_ESP8266
+  if (this->pin_ == 16 && mode == INPUT_PULLUP) {
+    // pullups are not available on GPIO16, manually override with
+    // input mode.
+    pinMode(16, INPUT);
+    return;
+  }
+#endif
+  pinMode(this->pin_, mode);
+}
+
+#ifdef ARDUINO_ARCH_ESP8266
+struct ESPHomeInterruptFuncInfo {
+  void (*func)(void *);
+  void *arg;
+};
+
+void ICACHE_RAM_ATTR interrupt_handler(void *arg) {
+  ArgStructure *as = static_cast<ArgStructure *>(arg);
+  auto *info = static_cast<ESPHomeInterruptFuncInfo *>(as->functionInfo);
+  info->func(info->arg);
+}
+#endif
+
+void GPIOPin::detach_interrupt() const { this->detach_interrupt_(); }
+void GPIOPin::detach_interrupt_() const {
+#ifdef ARDUINO_ARCH_ESP8266
+  __detachInterrupt(get_pin());
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+  detachInterrupt(get_pin());
+#endif
+}
+void GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, int mode) const {
+  if (this->inverted_) {
+    if (mode == RISING) {
+      mode = FALLING;
+    } else if (mode == FALLING) {
+      mode = RISING;
+    }
+  }
+#ifdef ARDUINO_ARCH_ESP8266
+  ArgStructure *as = new ArgStructure;
+  as->interruptInfo = nullptr;
+
+  as->functionInfo = new ESPHomeInterruptFuncInfo{
+      .func = func,
+      .arg = arg,
+  };
+
+  __attachInterruptArg(this->pin_, interrupt_handler, as, mode);
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+  // work around issue https://github.com/espressif/arduino-esp32/pull/1776 in arduino core
+  // yet again proves how horrible code is there :( - how could that have been accepted...
+  auto *attach = reinterpret_cast<void (*)(uint8_t, void (*)(void *), void *, int)>(attachInterruptArg);
+  attach(this->pin_, func, arg, mode);
+#endif
+}
+
+ISRInternalGPIOPin *GPIOPin::to_isr() const {
+  return new ISRInternalGPIOPin(this->pin_,
+#ifdef ARDUINO_ARCH_ESP32
+                                this->gpio_clear_, this->gpio_set_,
+#endif
+                                this->gpio_read_, this->gpio_mask_, this->inverted_);
+}
+
+void force_link_symbols() {
+#ifdef ARDUINO_ARCH_ESP8266
+  // Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible
+  // with their settings - ESPHome uses a different settings system (that can also survive
+  // erases). So set magic bytes indicating all tasmota versions are supported.
+  // This only adds 12 bytes of binary size, which is an acceptable price to pay for easier support
+  // for Tasmota.
+  // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/settings.ino#L346-L380
+  // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/i18n.h#L652-L654
+  const static uint32_t TASMOTA_MAGIC_BYTES[] PROGMEM = {0x5AA55AA5, 0xFFFFFFFF, 0xA55AA55A};
+  // Force link symbol by using a volatile integer (GCC attribute used does not work because of LTO)
+  volatile int x = 0;
+  x = TASMOTA_MAGIC_BYTES[x];
+#endif
+}
+
+}  // namespace esphome
+
+#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
+// Fix 2.3.0 std missing memchr
+extern "C" {
+void *memchr(const void *s, int c, size_t n) {
+  if (n == 0)
+    return nullptr;
+  const uint8_t *p = reinterpret_cast<const uint8_t *>(s);
+  do {
+    if (*p++ == c)
+      return const_cast<void *>(reinterpret_cast<const void *>(p - 1));
+  } while (--n != 0);
+  return nullptr;
+}
+};
+#endif
+
+#ifdef ARDUINO_ARCH_ESP8266
+extern "C" {
+extern void resetPins() {  // NOLINT
+  // Added in framework 2.7.0
+  // usually this sets up all pins to be in INPUT mode
+  // however, not strictly needed as we set up the pins properly
+  // ourselves and this causes pins to toggle during reboot.
+}
+}
+#endif

+ 127 - 0
livingroom/src/esphome/core/esphal.h

@@ -0,0 +1,127 @@
+#pragma once
+
+#include "Arduino.h"
+#ifdef ARDUINO_ARCH_ESP32
+#include <esp32-hal.h>
+#endif
+// Fix some arduino defs
+#ifdef round
+#undef round
+#endif
+#ifdef bool
+#undef bool
+#endif
+#ifdef true
+#undef true
+#endif
+#ifdef false
+#undef false
+#endif
+#ifdef min
+#undef min
+#endif
+#ifdef max
+#undef max
+#endif
+#ifdef abs
+#undef abs
+#endif
+
+namespace esphome {
+
+#define LOG_PIN(prefix, pin) \
+  if ((pin) != nullptr) { \
+    ESP_LOGCONFIG(TAG, prefix LOG_PIN_PATTERN, LOG_PIN_ARGS(pin)); \
+  }
+#define LOG_PIN_PATTERN "GPIO%u (Mode: %s%s)"
+#define LOG_PIN_ARGS(pin) (pin)->get_pin(), (pin)->get_pin_mode_name(), ((pin)->is_inverted() ? ", INVERTED" : "")
+
+/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions)
+class ISRInternalGPIOPin {
+ public:
+  ISRInternalGPIOPin(uint8_t pin,
+#ifdef ARDUINO_ARCH_ESP32
+                     volatile uint32_t *gpio_clear, volatile uint32_t *gpio_set,
+#endif
+                     volatile uint32_t *gpio_read, uint32_t gpio_mask, bool inverted);
+  bool digital_read();
+  void digital_write(bool value);
+  void clear_interrupt();
+
+ protected:
+  const uint8_t pin_;
+  const bool inverted_;
+  volatile uint32_t *const gpio_read_;
+  const uint32_t gpio_mask_;
+#ifdef ARDUINO_ARCH_ESP32
+  volatile uint32_t *const gpio_clear_;
+  volatile uint32_t *const gpio_set_;
+#endif
+};
+
+/** A high-level abstraction class that can expose a pin together with useful options like pinMode.
+ *
+ * Set the parameters for this at construction time and use setup() to apply them. The inverted parameter will
+ * automatically invert the input/output for you.
+ *
+ * Use read_value() and write_value() to use digitalRead() and digitalWrite(), respectively.
+ */
+class GPIOPin {
+ public:
+  /** Construct the GPIOPin instance.
+   *
+   * @param pin The GPIO pin number of this instance.
+   * @param mode The Arduino pinMode that this pin should be put into at setup().
+   * @param inverted Whether all digitalRead/digitalWrite calls should be inverted.
+   */
+  GPIOPin(uint8_t pin, uint8_t mode, bool inverted = false);
+
+  /// Setup the pin mode.
+  virtual void setup();
+  /// Read the binary value from this pin using digitalRead (and inverts automatically).
+  virtual bool digital_read();
+  /// Write the binary value to this pin using digitalWrite (and inverts automatically).
+  virtual void digital_write(bool value);
+  /// Set the pin mode
+  virtual void pin_mode(uint8_t mode);
+
+  /// Get the GPIO pin number.
+  uint8_t get_pin() const;
+  const char *get_pin_mode_name() const;
+  /// Get the pinMode of this pin.
+  uint8_t get_mode() const;
+  /// Return whether this pin shall be treated as inverted. (for example active-low)
+  bool is_inverted() const;
+
+  template<typename T> void attach_interrupt(void (*func)(T *), T *arg, int mode) const;
+  void detach_interrupt() const;
+
+  ISRInternalGPIOPin *to_isr() const;
+
+ protected:
+  void attach_interrupt_(void (*func)(void *), void *arg, int mode) const;
+  void detach_interrupt_() const;
+
+  const uint8_t pin_;
+  const uint8_t mode_;
+  const bool inverted_;
+#ifdef ARDUINO_ARCH_ESP32
+  volatile uint32_t *const gpio_set_;
+  volatile uint32_t *const gpio_clear_;
+#endif
+  volatile uint32_t *const gpio_read_;
+  const uint32_t gpio_mask_;
+};
+
+template<typename T> void GPIOPin::attach_interrupt(void (*func)(T *), T *arg, int mode) const {
+  this->attach_interrupt_(reinterpret_cast<void (*)(void *)>(func), arg, mode);
+}
+/** This function can be used by the HAL to force-link specific symbols
+ * into the generated binary without modifying the linker script.
+ *
+ * It is called by the application very early on startup and should not be used for anything
+ * other than forcing symbols to be linked.
+ */
+void force_link_symbols();
+
+}  // namespace esphome

+ 333 - 0
livingroom/src/esphome/core/helpers.cpp

@@ -0,0 +1,333 @@
+#include "esphome/core/helpers.h"
+#include <cstdio>
+#include <algorithm>
+
+#ifdef ARDUINO_ARCH_ESP8266
+#include <ESP8266WiFi.h>
+#else
+#include <Esp.h>
+#endif
+
+#include "esphome/core/log.h"
+#include "esphome/core/esphal.h"
+
+namespace esphome {
+
+static const char *TAG = "helpers";
+
+std::string get_mac_address() {
+  char tmp[20];
+  uint8_t mac[6];
+#ifdef ARDUINO_ARCH_ESP32
+  esp_efuse_mac_get_default(mac);
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+  WiFi.macAddress(mac);
+#endif
+  sprintf(tmp, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+  return std::string(tmp);
+}
+
+std::string get_mac_address_pretty() {
+  char tmp[20];
+  uint8_t mac[6];
+#ifdef ARDUINO_ARCH_ESP32
+  esp_efuse_mac_get_default(mac);
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+  WiFi.macAddress(mac);
+#endif
+  sprintf(tmp, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+  return std::string(tmp);
+}
+
+std::string generate_hostname(const std::string &base) { return base + std::string("-") + get_mac_address(); }
+
+uint32_t random_uint32() {
+#ifdef ARDUINO_ARCH_ESP32
+  return esp_random();
+#else
+  return os_random();
+#endif
+}
+
+double random_double() { return random_uint32() / double(UINT32_MAX); }
+
+float random_float() { return float(random_double()); }
+
+static uint32_t fast_random_seed = 0;
+
+void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; }
+uint32_t fast_random_32() {
+  fast_random_seed = (fast_random_seed * 2654435769ULL) + 40503ULL;
+  return fast_random_seed;
+}
+uint16_t fast_random_16() {
+  uint32_t rand32 = fast_random_32();
+  return (rand32 & 0xFFFF) + (rand32 >> 16);
+}
+uint8_t fast_random_8() {
+  uint8_t rand32 = fast_random_32();
+  return (rand32 & 0xFF) + ((rand32 >> 8) & 0xFF);
+}
+
+float gamma_correct(float value, float gamma) {
+  if (value <= 0.0f)
+    return 0.0f;
+  if (gamma <= 0.0f)
+    return value;
+
+  return powf(value, gamma);
+}
+std::string to_lowercase_underscore(std::string s) {
+  std::transform(s.begin(), s.end(), s.begin(), ::tolower);
+  std::replace(s.begin(), s.end(), ' ', '_');
+  return s;
+}
+
+std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist) {
+  std::string out(s);
+  out.erase(std::remove_if(out.begin(), out.end(),
+                           [&allowlist](const char &c) { return allowlist.find(c) == std::string::npos; }),
+            out.end());
+  return out;
+}
+
+std::string sanitize_hostname(const std::string &hostname) {
+  std::string s = sanitize_string_allowlist(hostname, HOSTNAME_CHARACTER_ALLOWLIST);
+  return truncate_string(s, 63);
+}
+
+std::string truncate_string(const std::string &s, size_t length) {
+  if (s.length() > length)
+    return s.substr(0, length);
+  return s;
+}
+
+std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
+  auto multiplier = float(pow10(accuracy_decimals));
+  float value_rounded = roundf(value * multiplier) / multiplier;
+  char tmp[32];  // should be enough, but we should maybe improve this at some point.
+  dtostrf(value_rounded, 0, uint8_t(std::max(0, int(accuracy_decimals))), tmp);
+  return std::string(tmp);
+}
+std::string uint64_to_string(uint64_t num) {
+  char buffer[17];
+  auto *address16 = reinterpret_cast<uint16_t *>(&num);
+  snprintf(buffer, sizeof(buffer), "%04X%04X%04X%04X", address16[3], address16[2], address16[1], address16[0]);
+  return std::string(buffer);
+}
+std::string uint32_to_string(uint32_t num) {
+  char buffer[9];
+  auto *address16 = reinterpret_cast<uint16_t *>(&num);
+  snprintf(buffer, sizeof(buffer), "%04X%04X", address16[1], address16[0]);
+  return std::string(buffer);
+}
+static char *global_json_build_buffer = nullptr;
+static size_t global_json_build_buffer_size = 0;
+
+void reserve_global_json_build_buffer(size_t required_size) {
+  if (global_json_build_buffer_size == 0 || global_json_build_buffer_size < required_size) {
+    delete[] global_json_build_buffer;
+    global_json_build_buffer_size = std::max(required_size, global_json_build_buffer_size * 2);
+
+    size_t remainder = global_json_build_buffer_size % 16U;
+    if (remainder != 0)
+      global_json_build_buffer_size += 16 - remainder;
+
+    global_json_build_buffer = new char[global_json_build_buffer_size];
+  }
+}
+
+ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
+  if (on == nullptr && strcasecmp(str, "on") == 0)
+    return PARSE_ON;
+  if (on != nullptr && strcasecmp(str, on) == 0)
+    return PARSE_ON;
+  if (off == nullptr && strcasecmp(str, "off") == 0)
+    return PARSE_OFF;
+  if (off != nullptr && strcasecmp(str, off) == 0)
+    return PARSE_OFF;
+  if (strcasecmp(str, "toggle") == 0)
+    return PARSE_TOGGLE;
+
+  return PARSE_NONE;
+}
+
+const char *HOSTNAME_CHARACTER_ALLOWLIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
+
+uint8_t crc8(uint8_t *data, uint8_t len) {
+  uint8_t crc = 0;
+
+  while ((len--) != 0u) {
+    uint8_t inbyte = *data++;
+    for (uint8_t i = 8; i != 0u; i--) {
+      bool mix = (crc ^ inbyte) & 0x01;
+      crc >>= 1;
+      if (mix)
+        crc ^= 0x8C;
+      inbyte >>= 1;
+    }
+  }
+  return crc;
+}
+
+void delay_microseconds_accurate(uint32_t usec) {
+  if (usec == 0)
+    return;
+  if (usec < 5000UL) {
+    delayMicroseconds(usec);
+    return;
+  }
+  uint32_t start = micros();
+  while (micros() - start < usec) {
+    delay(0);
+  }
+}
+
+uint8_t reverse_bits_8(uint8_t x) {
+  x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
+  x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2);
+  x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4);
+  return x;
+}
+
+uint16_t reverse_bits_16(uint16_t x) {
+  return uint16_t(reverse_bits_8(x & 0xFF) << 8) | uint16_t(reverse_bits_8(x >> 8));
+}
+std::string to_string(const std::string &val) { return val; }
+std::string to_string(int val) {
+  char buf[64];
+  sprintf(buf, "%d", val);
+  return buf;
+}
+std::string to_string(long val) {
+  char buf[64];
+  sprintf(buf, "%ld", val);
+  return buf;
+}
+std::string to_string(long long val) {
+  char buf[64];
+  sprintf(buf, "%lld", val);
+  return buf;
+}
+std::string to_string(unsigned val) {
+  char buf[64];
+  sprintf(buf, "%u", val);
+  return buf;
+}
+std::string to_string(unsigned long val) {
+  char buf[64];
+  sprintf(buf, "%lu", val);
+  return buf;
+}
+std::string to_string(unsigned long long val) {
+  char buf[64];
+  sprintf(buf, "%llu", val);
+  return buf;
+}
+std::string to_string(float val) {
+  char buf[64];
+  sprintf(buf, "%f", val);
+  return buf;
+}
+std::string to_string(double val) {
+  char buf[64];
+  sprintf(buf, "%f", val);
+  return buf;
+}
+std::string to_string(long double val) {
+  char buf[64];
+  sprintf(buf, "%Lf", val);
+  return buf;
+}
+optional<float> parse_float(const std::string &str) {
+  char *end;
+  float value = ::strtof(str.c_str(), &end);
+  if (end == nullptr || end != str.end().base())
+    return {};
+  return value;
+}
+uint32_t fnv1_hash(const std::string &str) {
+  uint32_t hash = 2166136261UL;
+  for (char c : str) {
+    hash *= 16777619UL;
+    hash ^= c;
+  }
+  return hash;
+}
+bool str_equals_case_insensitive(const std::string &a, const std::string &b) {
+  return strcasecmp(a.c_str(), b.c_str()) == 0;
+}
+
+template<uint32_t> uint32_t reverse_bits(uint32_t x) {
+  return uint32_t(reverse_bits_16(x & 0xFFFF) << 16) | uint32_t(reverse_bits_16(x >> 16));
+}
+
+static int high_freq_num_requests = 0;
+
+void HighFrequencyLoopRequester::start() {
+  if (this->started_)
+    return;
+  high_freq_num_requests++;
+  this->started_ = true;
+}
+void HighFrequencyLoopRequester::stop() {
+  if (!this->started_)
+    return;
+  high_freq_num_requests--;
+  this->started_ = false;
+}
+bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; }
+
+float clamp(float val, float min, float max) {
+  if (val < min)
+    return min;
+  if (val > max)
+    return max;
+  return val;
+}
+float lerp(float completion, float start, float end) { return start + (end - start) * completion; }
+
+bool str_startswith(const std::string &full, const std::string &start) { return full.rfind(start, 0) == 0; }
+bool str_endswith(const std::string &full, const std::string &ending) {
+  return full.rfind(ending) == (full.size() - ending.size());
+}
+
+uint16_t encode_uint16(uint8_t msb, uint8_t lsb) { return (uint16_t(msb) << 8) | uint16_t(lsb); }
+std::array<uint8_t, 2> decode_uint16(uint16_t value) {
+  uint8_t msb = (value >> 8) & 0xFF;
+  uint8_t lsb = (value >> 0) & 0xFF;
+  return {msb, lsb};
+}
+
+uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb) {
+  return (uint32_t(msb) << 24) | (uint32_t(byte2) << 16) | (uint32_t(byte3) << 8) | uint32_t(lsb);
+}
+
+std::string hexencode(const uint8_t *data, uint32_t len) {
+  char buf[20];
+  std::string res;
+  for (size_t i = 0; i < len; i++) {
+    if (i + 1 != len) {
+      sprintf(buf, "%02X.", data[i]);
+    } else {
+      sprintf(buf, "%02X ", data[i]);
+    }
+    res += buf;
+  }
+  sprintf(buf, "(%u)", len);
+  res += buf;
+  return res;
+}
+
+#ifdef ARDUINO_ARCH_ESP8266
+ICACHE_RAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); }
+ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); }
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+ICACHE_RAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
+ICACHE_RAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
+#endif
+
+}  // namespace esphome

+ 320 - 0
livingroom/src/esphome/core/helpers.h

@@ -0,0 +1,320 @@
+#pragma once
+
+#include <string>
+#include <functional>
+#include <vector>
+#include <memory>
+#include <type_traits>
+
+#include "esphome/core/optional.h"
+#include "esphome/core/esphal.h"
+
+#ifdef CLANG_TIDY
+#undef ICACHE_RAM_ATTR
+#define ICACHE_RAM_ATTR
+#undef ICACHE_RODATA_ATTR
+#define ICACHE_RODATA_ATTR
+#endif
+
+#define HOT __attribute__((hot))
+#define ESPDEPRECATED(msg) __attribute__((deprecated(msg)))
+#define ALWAYS_INLINE __attribute__((always_inline))
+#define PACKED __attribute__((packed))
+
+namespace esphome {
+
+/// The characters that are allowed in a hostname.
+extern const char *HOSTNAME_CHARACTER_ALLOWLIST;
+
+/// Gets the MAC address as a string, this can be used as way to identify this ESP.
+std::string get_mac_address();
+
+std::string get_mac_address_pretty();
+
+std::string to_string(const std::string &val);
+std::string to_string(int val);
+std::string to_string(long val);
+std::string to_string(long long val);
+std::string to_string(unsigned val);
+std::string to_string(unsigned long val);
+std::string to_string(unsigned long long val);
+std::string to_string(float val);
+std::string to_string(double val);
+std::string to_string(long double val);
+optional<float> parse_float(const std::string &str);
+
+/// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars.
+std::string sanitize_hostname(const std::string &hostname);
+
+/// Truncate a string to a specific length
+std::string truncate_string(const std::string &s, size_t length);
+
+/// Convert the string to lowercase_underscore.
+std::string to_lowercase_underscore(std::string s);
+
+/// Compare string a to string b (ignoring case) and return whether they are equal.
+bool str_equals_case_insensitive(const std::string &a, const std::string &b);
+bool str_startswith(const std::string &full, const std::string &start);
+bool str_endswith(const std::string &full, const std::string &ending);
+
+class HighFrequencyLoopRequester {
+ public:
+  void start();
+  void stop();
+
+  static bool is_high_frequency();
+
+ protected:
+  bool started_{false};
+};
+
+/** Clamp the value between min and max.
+ *
+ * @param val The value.
+ * @param min The minimum value.
+ * @param max The maximum value.
+ * @return val clamped in between min and max.
+ */
+float clamp(float val, float min, float max);
+
+/** Linearly interpolate between end start and end by completion.
+ *
+ * @tparam T The input/output typename.
+ * @param start The start value.
+ * @param end The end value.
+ * @param completion The completion. 0 is start value, 1 is end value.
+ * @return The linearly interpolated value.
+ */
+float lerp(float completion, float start, float end);
+
+/// std::make_unique
+template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args &&... args) {
+  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
+}
+
+/// Return a random 32 bit unsigned integer.
+uint32_t random_uint32();
+
+/** Returns a random double between 0 and 1.
+ *
+ * Note: This function probably doesn't provide a truly uniform distribution.
+ */
+double random_double();
+
+/// Returns a random float between 0 and 1. Essentially just casts random_double() to a float.
+float random_float();
+
+void fast_random_set_seed(uint32_t seed);
+uint32_t fast_random_32();
+uint16_t fast_random_16();
+uint8_t fast_random_8();
+
+/// Applies gamma correction with the provided gamma to value.
+float gamma_correct(float value, float gamma);
+
+/// Create a string from a value and an accuracy in decimals.
+std::string value_accuracy_to_string(float value, int8_t accuracy_decimals);
+
+/// Convert a uint64_t to a hex string
+std::string uint64_to_string(uint64_t num);
+
+/// Convert a uint32_t to a hex string
+std::string uint32_to_string(uint32_t num);
+
+/// Sanitizes the input string with the allowlist.
+std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist);
+
+uint8_t reverse_bits_8(uint8_t x);
+uint16_t reverse_bits_16(uint16_t x);
+uint32_t reverse_bits_32(uint32_t x);
+
+/// Encode a 16-bit unsigned integer given a most and least-significant byte.
+uint16_t encode_uint16(uint8_t msb, uint8_t lsb);
+/// Decode a 16-bit unsigned integer into an array of two values: most significant byte, least significant byte.
+std::array<uint8_t, 2> decode_uint16(uint16_t value);
+/// Encode a 32-bit unsigned integer given four bytes in MSB -> LSB order
+uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb);
+
+/***
+ * An interrupt helper class.
+ *
+ * This behaves like std::lock_guard. As long as the value is visible in the current stack, all interrupts
+ * (including flash reads) will be disabled.
+ *
+ * Please note all functions called when the interrupt lock must be marked ICACHE_RAM_ATTR (loading code into
+ * instruction cache is done via interrupts; disabling interrupts prevents data not already in cache from being
+ * pulled from flash).
+ *
+ * Example:
+ *
+ * ```cpp
+ * // interrupts are enabled
+ * {
+ *   InterruptLock lock;
+ *   // do something
+ *   // interrupts are disabled
+ * }
+ * // interrupts are enabled
+ * ```
+ */
+class InterruptLock {
+ public:
+  InterruptLock();
+  ~InterruptLock();
+
+ protected:
+#ifdef ARDUINO_ARCH_ESP8266
+  uint32_t xt_state_;
+#endif
+};
+
+/// Calculate a crc8 of data with the provided data length.
+uint8_t crc8(uint8_t *data, uint8_t len);
+
+enum ParseOnOffState {
+  PARSE_NONE = 0,
+  PARSE_ON,
+  PARSE_OFF,
+  PARSE_TOGGLE,
+};
+
+ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr);
+
+// Encode raw data to a human-readable string (for debugging)
+std::string hexencode(const uint8_t *data, uint32_t len);
+template<typename T> std::string hexencode(const T &data) { return hexencode(data.data(), data.size()); }
+
+// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
+template<int...> struct seq {};                                       // NOLINT
+template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {};  // NOLINT
+template<int... S> struct gens<0, S...> { using type = seq<S...>; };  // NOLINT
+
+template<bool B, class T = void> using enable_if_t = typename std::enable_if<B, T>::type;
+
+template<typename T, enable_if_t<!std::is_pointer<T>::value, int> = 0> T id(T value) { return value; }
+template<typename T, enable_if_t<std::is_pointer<T *>::value, int> = 0> T &id(T *value) { return *value; }
+
+template<typename... X> class CallbackManager;
+
+/** Simple helper class to allow having multiple subscribers to a signal.
+ *
+ * @tparam Ts The arguments for the callback, wrapped in void().
+ */
+template<typename... Ts> class CallbackManager<void(Ts...)> {
+ public:
+  /// Add a callback to the internal callback list.
+  void add(std::function<void(Ts...)> &&callback) { this->callbacks_.push_back(std::move(callback)); }
+
+  /// Call all callbacks in this manager.
+  void call(Ts... args) {
+    for (auto &cb : this->callbacks_)
+      cb(args...);
+  }
+
+ protected:
+  std::vector<std::function<void(Ts...)>> callbacks_;
+};
+
+// https://stackoverflow.com/a/37161919/8924614
+template<class T, class... Args>
+struct is_callable  // NOLINT
+{
+  template<class U> static auto test(U *p) -> decltype((*p)(std::declval<Args>()...), void(), std::true_type());
+
+  template<class U> static auto test(...) -> decltype(std::false_type());
+
+  static constexpr auto value = decltype(test<T>(nullptr))::value;  // NOLINT
+};
+
+template<typename T, typename... X> class TemplatableValue {
+ public:
+  TemplatableValue() : type_(EMPTY) {}
+
+  template<typename F, enable_if_t<!is_callable<F, X...>::value, int> = 0>
+  TemplatableValue(F value) : type_(VALUE), value_(value) {}
+
+  template<typename F, enable_if_t<is_callable<F, X...>::value, int> = 0>
+  TemplatableValue(F f) : type_(LAMBDA), f_(f) {}
+
+  bool has_value() { return this->type_ != EMPTY; }
+
+  T value(X... x) {
+    if (this->type_ == LAMBDA) {
+      return this->f_(x...);
+    }
+    // return value also when empty
+    return this->value_;
+  }
+
+  optional<T> optional_value(X... x) {
+    if (!this->has_value()) {
+      return {};
+    }
+    return this->value(x...);
+  }
+
+  T value_or(X... x, T default_value) {
+    if (!this->has_value()) {
+      return default_value;
+    }
+    return this->value(x...);
+  }
+
+ protected:
+  enum {
+    EMPTY,
+    VALUE,
+    LAMBDA,
+  } type_;
+
+  T value_;
+  std::function<T(X...)> f_;
+};
+
+template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
+ public:
+  TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
+
+  template<typename F, enable_if_t<!is_callable<F, X...>::value, int> = 0>
+  TemplatableStringValue(F value) : TemplatableValue<std::string, X...>(value) {}
+
+  template<typename F, enable_if_t<is_callable<F, X...>::value, int> = 0>
+  TemplatableStringValue(F f)
+      : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {}
+};
+
+void delay_microseconds_accurate(uint32_t usec);
+
+template<typename T> class Deduplicator {
+ public:
+  bool next(T value) {
+    if (this->has_value_) {
+      if (this->last_value_ == value)
+        return false;
+    }
+    this->has_value_ = true;
+    this->last_value_ = value;
+    return true;
+  }
+  bool has_value() const { return this->has_value_; }
+
+ protected:
+  bool has_value_{false};
+  T last_value_{};
+};
+
+template<typename T> class Parented {
+ public:
+  Parented() {}
+  Parented(T *parent) : parent_(parent) {}
+
+  T *get_parent() const { return parent_; }
+  void set_parent(T *parent) { parent_ = parent; }
+
+ protected:
+  T *parent_{nullptr};
+};
+
+uint32_t fnv1_hash(const std::string &str);
+
+}  // namespace esphome

+ 62 - 0
livingroom/src/esphome/core/log.cpp

@@ -0,0 +1,62 @@
+#include "log.h"
+#include "defines.h"
+#include "helpers.h"
+
+#ifdef USE_LOGGER
+#include "esphome/components/logger/logger.h"
+#endif
+
+namespace esphome {
+
+void HOT esp_log_printf_(int level, const char *tag, int line, const char *format, ...) {  // NOLINT
+  va_list arg;
+  va_start(arg, format);
+  esp_log_vprintf_(level, tag, line, format, arg);
+  va_end(arg);
+}
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+void HOT esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...) {
+  va_list arg;
+  va_start(arg, format);
+  esp_log_vprintf_(level, tag, line, format, arg);
+  va_end(arg);
+}
+#endif
+
+void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) {  // NOLINT
+#ifdef USE_LOGGER
+  auto *log = logger::global_logger;
+  if (log == nullptr)
+    return;
+
+  log->log_vprintf_(level, tag, line, format, args);
+#endif
+}
+
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
+                          va_list args) {  // NOLINT
+#ifdef USE_LOGGER
+  auto *log = logger::global_logger;
+  if (log == nullptr)
+    return;
+
+  log->log_vprintf_(level, tag, line, format, args);
+#endif
+}
+#endif
+
+#ifdef ARDUINO_ARCH_ESP32
+int HOT esp_idf_log_vprintf_(const char *format, va_list args) {  // NOLINT
+#ifdef USE_LOGGER
+  auto *log = logger::global_logger;
+  if (log == nullptr)
+    return 0;
+
+  log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args);
+#endif
+  return 0;
+}
+#endif
+
+}  // namespace esphome

+ 164 - 0
livingroom/src/esphome/core/log.h

@@ -0,0 +1,164 @@
+#pragma once
+
+#include <cassert>
+#include <cstdarg>
+#include <string>
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+#include "WString.h"
+#endif
+
+// avoid esp-idf redefining our macros
+#include "esphome/core/esphal.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+#include "esp_err.h"
+#endif
+
+namespace esphome {
+
+#define ESPHOME_LOG_LEVEL_NONE 0
+#define ESPHOME_LOG_LEVEL_ERROR 1
+#define ESPHOME_LOG_LEVEL_WARN 2
+#define ESPHOME_LOG_LEVEL_INFO 3
+#define ESPHOME_LOG_LEVEL_CONFIG 4
+#define ESPHOME_LOG_LEVEL_DEBUG 5
+#define ESPHOME_LOG_LEVEL_VERBOSE 6
+#define ESPHOME_LOG_LEVEL_VERY_VERBOSE 7
+
+#ifndef ESPHOME_LOG_LEVEL
+#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_DEBUG
+#endif
+
+#define ESPHOME_LOG_COLOR_BLACK "30"
+#define ESPHOME_LOG_COLOR_RED "31"     // ERROR
+#define ESPHOME_LOG_COLOR_GREEN "32"   // INFO
+#define ESPHOME_LOG_COLOR_YELLOW "33"  // WARNING
+#define ESPHOME_LOG_COLOR_BLUE "34"
+#define ESPHOME_LOG_COLOR_MAGENTA "35"  // CONFIG
+#define ESPHOME_LOG_COLOR_CYAN "36"     // DEBUG
+#define ESPHOME_LOG_COLOR_GRAY "37"     // VERBOSE
+#define ESPHOME_LOG_COLOR_WHITE "38"
+#define ESPHOME_LOG_SECRET_BEGIN "\033[5m"
+#define ESPHOME_LOG_SECRET_END "\033[6m"
+#define LOG_SECRET(x) ESPHOME_LOG_SECRET_BEGIN x ESPHOME_LOG_SECRET_END
+
+#define ESPHOME_LOG_COLOR(COLOR) "\033[0;" COLOR "m"
+#define ESPHOME_LOG_BOLD(COLOR) "\033[1;" COLOR "m"
+#define ESPHOME_LOG_RESET_COLOR "\033[0m"
+
+void esp_log_printf_(int level, const char *tag, int line, const char *format, ...)  // NOLINT
+    __attribute__((format(printf, 4, 5)));
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+void esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...);
+#endif
+void esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args);  // NOLINT
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+void esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args);
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
+#endif
+
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+#define ESPHOME_LOG_FORMAT(format) F(format)
+#else
+#define ESPHOME_LOG_FORMAT(format) format
+#endif
+
+#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
+#define esph_log_vv(tag, format, ...) \
+  esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
+
+#define ESPHOME_LOG_HAS_VERY_VERBOSE
+#else
+#define esph_log_vv(tag, format, ...)
+#endif
+
+#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
+#define esph_log_v(tag, format, ...) \
+  esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
+
+#define ESPHOME_LOG_HAS_VERBOSE
+#else
+#define esph_log_v(tag, format, ...)
+#endif
+
+#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
+#define esph_log_d(tag, format, ...) \
+  esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
+#define esph_log_config(tag, format, ...) \
+  esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
+
+#define ESPHOME_LOG_HAS_DEBUG
+#define ESPHOME_LOG_HAS_CONFIG
+#else
+#define esph_log_d(tag, format, ...)
+#define esph_log_config(tag, format, ...)
+#endif
+
+#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
+#define esph_log_i(tag, format, ...) \
+  esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
+
+#define ESPHOME_LOG_HAS_INFO
+#else
+#define esph_log_i(tag, format, ...)
+#endif
+
+#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
+#define esph_log_w(tag, format, ...) \
+  esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
+
+#define ESPHOME_LOG_HAS_WARN
+#else
+#define esph_log_w(tag, format, ...)
+#endif
+
+#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
+#define esph_log_e(tag, format, ...) \
+  esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
+
+#define ESPHOME_LOG_HAS_ERROR
+#else
+#define esph_log_e(tag, format, ...)
+#endif
+
+#ifdef ESP_LOGE
+#undef ESP_LOGE
+#endif
+#ifdef ESP_LOGW
+#undef ESP_LOGW
+#endif
+#ifdef ESP_LOGI
+#undef ESP_LOGI
+#endif
+#ifdef ESP_LOGD
+#undef ESP_LOGD
+#endif
+#ifdef ESP_LOGV
+#undef ESP_LOGV
+#endif
+
+#define ESP_LOGE(tag, ...) esph_log_e(tag, __VA_ARGS__)
+#define LOG_E(tag, ...) ESP_LOGE(tag, __VA__ARGS__)
+#define ESP_LOGW(tag, ...) esph_log_w(tag, __VA_ARGS__)
+#define LOG_W(tag, ...) ESP_LOGW(tag, __VA__ARGS__)
+#define ESP_LOGI(tag, ...) esph_log_i(tag, __VA_ARGS__)
+#define LOG_I(tag, ...) ESP_LOGI(tag, __VA__ARGS__)
+#define ESP_LOGD(tag, ...) esph_log_d(tag, __VA_ARGS__)
+#define LOG_D(tag, ...) ESP_LOGD(tag, __VA__ARGS__)
+#define ESP_LOGCONFIG(tag, ...) esph_log_config(tag, __VA_ARGS__)
+#define LOG_CONFIG(tag, ...) ESP_LOGCONFIG(tag, __VA__ARGS__)
+#define ESP_LOGV(tag, ...) esph_log_v(tag, __VA_ARGS__)
+#define LOG_V(tag, ...) ESP_LOGV(tag, __VA__ARGS__)
+#define ESP_LOGVV(tag, ...) esph_log_vv(tag, __VA_ARGS__)
+#define LOG_VV(tag, ...) ESP_LOGVV(tag, __VA__ARGS__)
+
+#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c"
+#define BYTE_TO_BINARY(byte) \
+  ((byte) &0x80 ? '1' : '0'), ((byte) &0x40 ? '1' : '0'), ((byte) &0x20 ? '1' : '0'), ((byte) &0x10 ? '1' : '0'), \
+      ((byte) &0x08 ? '1' : '0'), ((byte) &0x04 ? '1' : '0'), ((byte) &0x02 ? '1' : '0'), ((byte) &0x01 ? '1' : '0')
+#define YESNO(b) ((b) ? "YES" : "NO")
+#define ONOFF(b) ((b) ? "ON" : "OFF")
+
+}  // namespace esphome

+ 214 - 0
livingroom/src/esphome/core/optional.h

@@ -0,0 +1,214 @@
+#pragma once
+//
+// Copyright (c) 2017 Martin Moene
+//
+// https://github.com/martinmoene/optional-bare
+//
+// This code is licensed under the MIT License (MIT).
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+// Modified by Otto Winter on 18.05.18
+
+namespace esphome {
+
+// type for nullopt
+
+struct nullopt_t {  // NOLINT
+  struct init {};   // NOLINT
+  nullopt_t(init) {}
+};
+
+// extra parenthesis to prevent the most vexing parse:
+
+const nullopt_t nullopt((nullopt_t::init()));  // NOLINT
+
+// Simplistic optional: requires T to be default constructible, copyable.
+
+template<typename T> class optional {  // NOLINT
+ private:
+  using safe_bool = void (optional::*)() const;
+
+ public:
+  using value_type = T;
+
+  optional() {}
+
+  optional(nullopt_t) {}
+
+  optional(T const &arg) : has_value_(true), value_(arg) {}
+
+  template<class U> optional(optional<U> const &other) : has_value_(other.has_value()), value_(other.value()) {}
+
+  optional &operator=(nullopt_t) {
+    reset();
+    return *this;
+  }
+
+  template<class U> optional &operator=(optional<U> const &other) {
+    has_value_ = other.has_value();
+    value_ = other.value();
+    return *this;
+  }
+
+  void swap(optional &rhs) {
+    using std::swap;
+    if (has_value() && rhs.has_value()) {
+      swap(**this, *rhs);
+    } else if (!has_value() && rhs.has_value()) {
+      initialize(*rhs);
+      rhs.reset();
+    } else if (has_value() && !rhs.has_value()) {
+      rhs.initialize(**this);
+      reset();
+    }
+  }
+
+  // observers
+
+  value_type const *operator->() const { return &value_; }
+
+  value_type *operator->() { return &value_; }
+
+  value_type const &operator*() const { return value_; }
+
+  value_type &operator*() { return value_; }
+
+  operator safe_bool() const { return has_value() ? &optional::this_type_does_not_support_comparisons : nullptr; }
+
+  bool has_value() const { return has_value_; }
+
+  value_type const &value() const { return value_; }
+
+  value_type &value() { return value_; }
+
+  template<class U> value_type value_or(U const &v) const { return has_value() ? value() : static_cast<value_type>(v); }
+
+  // modifiers
+
+  void reset() { has_value_ = false; }
+
+ private:
+  void this_type_does_not_support_comparisons() const {}  // NOLINT
+
+  template<typename V> void initialize(V const &value) {  // NOLINT
+    value_ = value;
+    has_value_ = true;
+  }
+
+ private:
+  bool has_value_{false};  // NOLINT
+  value_type value_;       // NOLINT
+};
+
+// Relational operators
+
+template<typename T, typename U> inline bool operator==(optional<T> const &x, optional<U> const &y) {
+  return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y;
+}
+
+template<typename T, typename U> inline bool operator!=(optional<T> const &x, optional<U> const &y) {
+  return !(x == y);
+}
+
+template<typename T, typename U> inline bool operator<(optional<T> const &x, optional<U> const &y) {
+  return (!y) ? false : (!x) ? true : *x < *y;
+}
+
+template<typename T, typename U> inline bool operator>(optional<T> const &x, optional<U> const &y) { return (y < x); }
+
+template<typename T, typename U> inline bool operator<=(optional<T> const &x, optional<U> const &y) { return !(y < x); }
+
+template<typename T, typename U> inline bool operator>=(optional<T> const &x, optional<U> const &y) { return !(x < y); }
+
+// Comparison with nullopt
+
+template<typename T> inline bool operator==(optional<T> const &x, nullopt_t) { return (!x); }
+
+template<typename T> inline bool operator==(nullopt_t, optional<T> const &x) { return (!x); }
+
+template<typename T> inline bool operator!=(optional<T> const &x, nullopt_t) { return bool(x); }
+
+template<typename T> inline bool operator!=(nullopt_t, optional<T> const &x) { return bool(x); }
+
+template<typename T> inline bool operator<(optional<T> const &, nullopt_t) { return false; }
+
+template<typename T> inline bool operator<(nullopt_t, optional<T> const &x) { return bool(x); }
+
+template<typename T> inline bool operator<=(optional<T> const &x, nullopt_t) { return (!x); }
+
+template<typename T> inline bool operator<=(nullopt_t, optional<T> const &) { return true; }
+
+template<typename T> inline bool operator>(optional<T> const &x, nullopt_t) { return bool(x); }
+
+template<typename T> inline bool operator>(nullopt_t, optional<T> const &) { return false; }
+
+template<typename T> inline bool operator>=(optional<T> const &, nullopt_t) { return true; }
+
+template<typename T> inline bool operator>=(nullopt_t, optional<T> const &x) { return (!x); }
+
+// Comparison with T
+
+template<typename T, typename U> inline bool operator==(optional<T> const &x, U const &v) {
+  return bool(x) ? *x == v : false;
+}
+
+template<typename T, typename U> inline bool operator==(U const &v, optional<T> const &x) {
+  return bool(x) ? v == *x : false;
+}
+
+template<typename T, typename U> inline bool operator!=(optional<T> const &x, U const &v) {
+  return bool(x) ? *x != v : true;
+}
+
+template<typename T, typename U> inline bool operator!=(U const &v, optional<T> const &x) {
+  return bool(x) ? v != *x : true;
+}
+
+template<typename T, typename U> inline bool operator<(optional<T> const &x, U const &v) {
+  return bool(x) ? *x < v : true;
+}
+
+template<typename T, typename U> inline bool operator<(U const &v, optional<T> const &x) {
+  return bool(x) ? v < *x : false;
+}
+
+template<typename T, typename U> inline bool operator<=(optional<T> const &x, U const &v) {
+  return bool(x) ? *x <= v : true;
+}
+
+template<typename T, typename U> inline bool operator<=(U const &v, optional<T> const &x) {
+  return bool(x) ? v <= *x : false;
+}
+
+template<typename T, typename U> inline bool operator>(optional<T> const &x, U const &v) {
+  return bool(x) ? *x > v : false;
+}
+
+template<typename T, typename U> inline bool operator>(U const &v, optional<T> const &x) {
+  return bool(x) ? v > *x : true;
+}
+
+template<typename T, typename U> inline bool operator>=(optional<T> const &x, U const &v) {
+  return bool(x) ? *x >= v : false;
+}
+
+template<typename T, typename U> inline bool operator>=(U const &v, optional<T> const &x) {
+  return bool(x) ? v >= *x : true;
+}
+
+// Specialized algorithms
+
+template<typename T> void swap(optional<T> &x, optional<T> &y) { x.swap(y); }
+
+// Convenience function to create an optional.
+
+template<typename T> inline optional<T> make_optional(T const &v) { return optional<T>(v); }
+
+}  // namespace esphome

+ 308 - 0
livingroom/src/esphome/core/preferences.cpp

@@ -0,0 +1,308 @@
+#include "esphome/core/preferences.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/application.h"
+
+#ifdef ARDUINO_ARCH_ESP8266
+extern "C" {
+#include "spi_flash.h"
+}
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+#include "nvs.h"
+#include "nvs_flash.h"
+#endif
+
+namespace esphome {
+
+static const char *TAG = "preferences";
+
+ESPPreferenceObject::ESPPreferenceObject() : offset_(0), length_words_(0), type_(0), data_(nullptr) {}
+ESPPreferenceObject::ESPPreferenceObject(size_t offset, size_t length, uint32_t type)
+    : offset_(offset), length_words_(length), type_(type) {
+  this->data_ = new uint32_t[this->length_words_ + 1];
+  for (uint32_t i = 0; i < this->length_words_ + 1; i++)
+    this->data_[i] = 0;
+}
+bool ESPPreferenceObject::load_() {
+  if (!this->is_initialized()) {
+    ESP_LOGV(TAG, "Load Pref Not initialized!");
+    return false;
+  }
+  if (!this->load_internal_())
+    return false;
+
+  bool valid = this->data_[this->length_words_] == this->calculate_crc_();
+
+  ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_,  // NOLINT
+            YESNO(valid), this->data_[0], this->data_[1], this->type_, this->calculate_crc_());
+  return valid;
+}
+bool ESPPreferenceObject::save_() {
+  if (!this->is_initialized()) {
+    ESP_LOGV(TAG, "Save Pref Not initialized!");
+    return false;
+  }
+
+  this->data_[this->length_words_] = this->calculate_crc_();
+  if (!this->save_internal_())
+    return false;
+  ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_,  // NOLINT
+            this->data_[0], this->data_[1], this->type_, this->calculate_crc_());
+  return true;
+}
+
+#ifdef ARDUINO_ARCH_ESP8266
+
+static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
+#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
+static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
+static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
+
+#ifdef USE_ESP8266_PREFERENCES_FLASH
+static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
+#else
+static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
+#endif
+
+static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
+  if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
+    return false;
+  }
+  *dest = ESP_RTC_USER_MEM[index];
+  return true;
+}
+
+static bool esp8266_flash_dirty = false;
+
+static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
+  if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
+    return false;
+  }
+  if (index < 32 && global_preferences.is_prevent_write()) {
+    return false;
+  }
+
+  auto *ptr = &ESP_RTC_USER_MEM[index];
+  *ptr = value;
+  return true;
+}
+
+extern "C" uint32_t _SPIFFS_end;
+
+static const uint32_t get_esp8266_flash_sector() {
+  union {
+    uint32_t *ptr;
+    uint32_t uint;
+  } data{};
+  data.ptr = &_SPIFFS_end;
+  return (data.uint - 0x40200000) / SPI_FLASH_SEC_SIZE;
+}
+static const uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; }
+
+void ESPPreferences::save_esp8266_flash_() {
+  if (!esp8266_flash_dirty)
+    return;
+
+  ESP_LOGVV(TAG, "Saving preferences to flash...");
+  SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK;
+  {
+    InterruptLock lock;
+    erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
+    if (erase_res == SPI_FLASH_RESULT_OK) {
+      write_res = spi_flash_write(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4);
+    }
+  }
+  if (erase_res != SPI_FLASH_RESULT_OK) {
+    ESP_LOGV(TAG, "Erase ESP8266 flash failed!");
+    return;
+  }
+  if (write_res != SPI_FLASH_RESULT_OK) {
+    ESP_LOGV(TAG, "Write ESP8266 flash failed!");
+    return;
+  }
+
+  esp8266_flash_dirty = false;
+}
+
+bool ESPPreferenceObject::save_internal_() {
+  if (this->in_flash_) {
+    for (uint32_t i = 0; i <= this->length_words_; i++) {
+      uint32_t j = this->offset_ + i;
+      if (j >= ESP8266_FLASH_STORAGE_SIZE)
+        return false;
+      uint32_t v = this->data_[i];
+      uint32_t *ptr = &global_preferences.flash_storage_[j];
+      if (*ptr != v)
+        esp8266_flash_dirty = true;
+      *ptr = v;
+    }
+    global_preferences.save_esp8266_flash_();
+    return true;
+  }
+
+  for (uint32_t i = 0; i <= this->length_words_; i++) {
+    if (!esp_rtc_user_mem_write(this->offset_ + i, this->data_[i]))
+      return false;
+  }
+
+  return true;
+}
+bool ESPPreferenceObject::load_internal_() {
+  if (this->in_flash_) {
+    for (uint32_t i = 0; i <= this->length_words_; i++) {
+      uint32_t j = this->offset_ + i;
+      if (j >= ESP8266_FLASH_STORAGE_SIZE)
+        return false;
+      this->data_[i] = global_preferences.flash_storage_[j];
+    }
+
+    return true;
+  }
+
+  for (uint32_t i = 0; i <= this->length_words_; i++) {
+    if (!esp_rtc_user_mem_read(this->offset_ + i, &this->data_[i]))
+      return false;
+  }
+  return true;
+}
+ESPPreferences::ESPPreferences()
+    // offset starts from start of user RTC mem (64 words before that are reserved for system),
+    // an additional 32 words at the start of user RTC are for eboot (OTA, see eboot_command.h),
+    // which will be reset each time OTA occurs
+    : current_offset_(0) {}
+
+void ESPPreferences::begin() {
+  this->flash_storage_ = new uint32_t[ESP8266_FLASH_STORAGE_SIZE];
+  ESP_LOGVV(TAG, "Loading preferences from flash...");
+
+  {
+    InterruptLock lock;
+    spi_flash_read(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4);
+  }
+}
+
+ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) {
+  if (in_flash) {
+    uint32_t start = this->current_flash_offset_;
+    uint32_t end = start + length + 1;
+    if (end > ESP8266_FLASH_STORAGE_SIZE)
+      return {};
+    auto pref = ESPPreferenceObject(start, length, type);
+    pref.in_flash_ = true;
+    this->current_flash_offset_ = end;
+    return pref;
+  }
+
+  uint32_t start = this->current_offset_;
+  uint32_t end = start + length + 1;
+  bool in_normal = start < 96;
+  // Normal: offset 0-95 maps to RTC offset 32 - 127,
+  // Eboot: offset 96-127 maps to RTC offset 0 - 31 words
+  if (in_normal && end > 96) {
+    // start is in normal but end is not -> switch to Eboot
+    this->current_offset_ = start = 96;
+    end = start + length + 1;
+    in_normal = false;
+  }
+
+  if (end > 128) {
+    // Doesn't fit in data, return uninitialized preference obj.
+    return {};
+  }
+
+  uint32_t rtc_offset;
+  if (in_normal) {
+    rtc_offset = start + 32;
+  } else {
+    rtc_offset = start - 96;
+  }
+
+  auto pref = ESPPreferenceObject(rtc_offset, length, type);
+  this->current_offset_ += length + 1;
+  return pref;
+}
+void ESPPreferences::prevent_write(bool prevent) { this->prevent_write_ = prevent; }
+bool ESPPreferences::is_prevent_write() { return this->prevent_write_; }
+#endif
+
+#ifdef ARDUINO_ARCH_ESP32
+bool ESPPreferenceObject::save_internal_() {
+  if (global_preferences.nvs_handle_ == 0)
+    return false;
+
+  char key[32];
+  sprintf(key, "%u", this->offset_);
+  uint32_t len = (this->length_words_ + 1) * 4;
+  esp_err_t err = nvs_set_blob(global_preferences.nvs_handle_, key, this->data_, len);
+  if (err) {
+    ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", key, len, esp_err_to_name(err));
+    return false;
+  }
+  err = nvs_commit(global_preferences.nvs_handle_);
+  if (err) {
+    ESP_LOGV(TAG, "nvs_commit('%s', len=%u) failed: %s", key, len, esp_err_to_name(err));
+    return false;
+  }
+  return true;
+}
+bool ESPPreferenceObject::load_internal_() {
+  if (global_preferences.nvs_handle_ == 0)
+    return false;
+
+  char key[32];
+  sprintf(key, "%u", this->offset_);
+  uint32_t len = (this->length_words_ + 1) * 4;
+
+  uint32_t actual_len;
+  esp_err_t err = nvs_get_blob(global_preferences.nvs_handle_, key, nullptr, &actual_len);
+  if (err) {
+    ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key, esp_err_to_name(err));
+    return false;
+  }
+  if (actual_len != len) {
+    ESP_LOGVV(TAG, "NVS length does not match. Assuming key changed (%u!=%u)", actual_len, len);
+    return false;
+  }
+  err = nvs_get_blob(global_preferences.nvs_handle_, key, this->data_, &len);
+  if (err) {
+    ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key, esp_err_to_name(err));
+    return false;
+  }
+  return true;
+}
+ESPPreferences::ESPPreferences() : current_offset_(0) {}
+void ESPPreferences::begin() {
+  auto ns = truncate_string(App.get_name(), 15);
+  esp_err_t err = nvs_open(ns.c_str(), NVS_READWRITE, &this->nvs_handle_);
+  if (err) {
+    ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS...", esp_err_to_name(err));
+    nvs_flash_deinit();
+    nvs_flash_erase();
+    nvs_flash_init();
+
+    err = nvs_open(ns.c_str(), NVS_READWRITE, &this->nvs_handle_);
+    if (err) {
+      this->nvs_handle_ = 0;
+    }
+  }
+}
+
+ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) {
+  auto pref = ESPPreferenceObject(this->current_offset_, length, type);
+  this->current_offset_++;
+  return pref;
+}
+#endif
+uint32_t ESPPreferenceObject::calculate_crc_() const {
+  uint32_t crc = this->type_;
+  for (size_t i = 0; i < this->length_words_; i++) {
+    crc ^= (this->data_[i] * 2654435769UL) >> 1;
+  }
+  return crc;
+}
+bool ESPPreferenceObject::is_initialized() const { return this->data_ != nullptr; }
+
+ESPPreferences global_preferences;
+
+}  // namespace esphome

+ 109 - 0
livingroom/src/esphome/core/preferences.h

@@ -0,0 +1,109 @@
+#pragma once
+
+#include <string>
+
+#include "esphome/core/esphal.h"
+#include "esphome/core/defines.h"
+
+namespace esphome {
+
+class ESPPreferenceObject {
+ public:
+  ESPPreferenceObject();
+  ESPPreferenceObject(size_t offset, size_t length, uint32_t type);
+
+  template<typename T> bool save(T *src);
+
+  template<typename T> bool load(T *dest);
+
+  bool is_initialized() const;
+
+ protected:
+  friend class ESPPreferences;
+
+  bool save_();
+  bool load_();
+  bool save_internal_();
+  bool load_internal_();
+
+  uint32_t calculate_crc_() const;
+
+  size_t offset_;
+  size_t length_words_;
+  uint32_t type_;
+  uint32_t *data_;
+#ifdef ARDUINO_ARCH_ESP8266
+  bool in_flash_{false};
+#endif
+};
+
+#ifdef ARDUINO_ARCH_ESP8266
+#ifdef USE_ESP8266_PREFERENCES_FLASH
+static bool DEFAULT_IN_FLASH = true;
+#else
+static bool DEFAULT_IN_FLASH = false;
+#endif
+#endif
+
+#ifdef ARDUINO_ARCH_ESP32
+static bool DEFAULT_IN_FLASH = true;
+#endif
+
+class ESPPreferences {
+ public:
+  ESPPreferences();
+  void begin();
+  ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash = DEFAULT_IN_FLASH);
+  template<typename T> ESPPreferenceObject make_preference(uint32_t type, bool in_flash = DEFAULT_IN_FLASH);
+
+#ifdef ARDUINO_ARCH_ESP8266
+  /** On the ESP8266, we can't override the first 128 bytes during OTA uploads
+   * as the eboot parameters are stored there. Writing there during an OTA upload
+   * would invalidate applying the new firmware. During normal operation, we use
+   * this part of the RTC user memory, but stop writing to it during OTA uploads.
+   *
+   * @param prevent Whether to prevent writing to the first 32 words of RTC user memory.
+   */
+  void prevent_write(bool prevent);
+  bool is_prevent_write();
+#endif
+
+ protected:
+  friend ESPPreferenceObject;
+
+  uint32_t current_offset_;
+#ifdef ARDUINO_ARCH_ESP32
+  uint32_t nvs_handle_;
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+  void save_esp8266_flash_();
+  bool prevent_write_{false};
+  uint32_t *flash_storage_;
+  uint32_t current_flash_offset_;
+#endif
+};
+
+extern ESPPreferences global_preferences;
+
+template<typename T> ESPPreferenceObject ESPPreferences::make_preference(uint32_t type, bool in_flash) {
+  return this->make_preference((sizeof(T) + 3) / 4, type, in_flash);
+}
+
+template<typename T> bool ESPPreferenceObject::save(T *src) {
+  if (!this->is_initialized())
+    return false;
+  memset(this->data_, 0, this->length_words_ * 4);
+  memcpy(this->data_, src, sizeof(T));
+  return this->save_();
+}
+
+template<typename T> bool ESPPreferenceObject::load(T *dest) {
+  memset(this->data_, 0, this->length_words_ * 4);
+  if (!this->load_())
+    return false;
+
+  memcpy(dest, this->data_, sizeof(T));
+  return true;
+}
+
+}  // namespace esphome

+ 271 - 0
livingroom/src/esphome/core/scheduler.cpp

@@ -0,0 +1,271 @@
+#include "scheduler.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+#include <algorithm>
+
+namespace esphome {
+
+static const char *TAG = "scheduler";
+
+static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
+static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
+
+// Uncomment to debug scheduler
+// #define ESPHOME_DEBUG_SCHEDULER
+
+void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
+                                std::function<void()> &&func) {
+  const uint32_t now = this->millis_();
+
+  if (!name.empty())
+    this->cancel_timeout(component, name);
+
+  if (timeout == SCHEDULER_DONT_RUN)
+    return;
+
+  ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout);
+
+  auto item = make_unique<SchedulerItem>();
+  item->component = component;
+  item->name = name;
+  item->type = SchedulerItem::TIMEOUT;
+  item->timeout = timeout;
+  item->last_execution = now;
+  item->last_execution_major = this->millis_major_;
+  item->f = std::move(func);
+  item->remove = false;
+  this->push_(std::move(item));
+}
+bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) {
+  return this->cancel_item_(component, name, SchedulerItem::TIMEOUT);
+}
+void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval,
+                                 std::function<void()> &&func) {
+  const uint32_t now = this->millis_();
+
+  if (!name.empty())
+    this->cancel_interval(component, name);
+
+  if (interval == SCHEDULER_DONT_RUN)
+    return;
+
+  // only put offset in lower half
+  uint32_t offset = 0;
+  if (interval != 0)
+    offset = (random_uint32() % interval) / 2;
+
+  ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset);
+
+  auto item = make_unique<SchedulerItem>();
+  item->component = component;
+  item->name = name;
+  item->type = SchedulerItem::INTERVAL;
+  item->interval = interval;
+  item->last_execution = now - offset - interval;
+  item->last_execution_major = this->millis_major_;
+  if (item->last_execution > now)
+    item->last_execution_major--;
+  item->f = std::move(func);
+  item->remove = false;
+  this->push_(std::move(item));
+}
+bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
+  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
+}
+optional<uint32_t> HOT Scheduler::next_schedule_in() {
+  if (this->empty_())
+    return {};
+  auto &item = this->items_[0];
+  const uint32_t now = this->millis_();
+  uint32_t next_time = item->last_execution + item->interval;
+  if (next_time < now)
+    return 0;
+  return next_time - now;
+}
+void ICACHE_RAM_ATTR HOT Scheduler::call() {
+  const uint32_t now = this->millis_();
+  this->process_to_add();
+
+#ifdef ESPHOME_DEBUG_SCHEDULER
+  static uint32_t last_print = 0;
+
+  if (now - last_print > 2000) {
+    last_print = now;
+    std::vector<std::unique_ptr<SchedulerItem>> old_items;
+    ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now);
+    while (!this->empty_()) {
+      auto item = std::move(this->items_[0]);
+      const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout";
+      ESP_LOGVV(TAG, "  %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", type, item->name.c_str(),
+                item->interval, item->last_execution, item->last_execution_major, item->next_execution(),
+                item->next_execution_major());
+
+      this->pop_raw_();
+      old_items.push_back(std::move(item));
+    }
+    ESP_LOGVV(TAG, "\n");
+    this->items_ = std::move(old_items);
+  }
+#endif  // ESPHOME_DEBUG_SCHEDULER
+
+  auto to_remove_was = to_remove_;
+  auto items_was = items_.size();
+  // If we have too many items to remove
+  if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
+    std::vector<std::unique_ptr<SchedulerItem>> valid_items;
+    while (!this->empty_()) {
+      auto item = std::move(this->items_[0]);
+      this->pop_raw_();
+      valid_items.push_back(std::move(item));
+    }
+    this->items_ = std::move(valid_items);
+
+    // The following should not happen unless I'm missing something
+    if (to_remove_ != 0) {
+      ESP_LOGW(TAG, "to_remove_ was %u now: %u items where %zu now %zu. Please report this", to_remove_was, to_remove_,
+               items_was, items_.size());
+      to_remove_ = 0;
+    }
+  }
+
+  while (!this->empty_()) {
+    // use scoping to indicate visibility of `item` variable
+    {
+      // Don't copy-by value yet
+      auto &item = this->items_[0];
+      if ((now - item->last_execution) < item->interval)
+        // Not reached timeout yet, done for this call
+        break;
+      uint8_t major = item->next_execution_major();
+      if (this->millis_major_ - major > 1)
+        break;
+
+      // Don't run on failed components
+      if (item->component != nullptr && item->component->is_failed()) {
+        this->pop_raw_();
+        continue;
+      }
+
+#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
+      const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout";
+      ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", type, item->name.c_str(),
+                item->interval, item->last_execution, now);
+#endif
+
+      // Warning: During f(), a lot of stuff can happen, including:
+      //  - timeouts/intervals get added, potentially invalidating vector pointers
+      //  - timeouts/intervals get cancelled
+      item->f();
+    }
+
+    {
+      // new scope, item from before might have been moved in the vector
+      auto item = std::move(this->items_[0]);
+
+      // Only pop after function call, this ensures we were reachable
+      // during the function call and know if we were cancelled.
+      this->pop_raw_();
+
+      if (item->remove) {
+        // We were removed/cancelled in the function call, stop
+        to_remove_--;
+        continue;
+      }
+
+      if (item->type == SchedulerItem::INTERVAL) {
+        if (item->interval != 0) {
+          const uint32_t before = item->last_execution;
+          const uint32_t amount = (now - item->last_execution) / item->interval;
+          item->last_execution += amount * item->interval;
+          if (item->last_execution < before)
+            item->last_execution_major++;
+        }
+        this->push_(std::move(item));
+      }
+    }
+  }
+
+  this->process_to_add();
+}
+void HOT Scheduler::process_to_add() {
+  for (auto &it : this->to_add_) {
+    if (it->remove) {
+      continue;
+    }
+
+    this->items_.push_back(std::move(it));
+    std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
+  }
+  this->to_add_.clear();
+}
+void HOT Scheduler::cleanup_() {
+  while (!this->items_.empty()) {
+    auto &item = this->items_[0];
+    if (!item->remove)
+      return;
+
+    to_remove_--;
+    this->pop_raw_();
+  }
+}
+void HOT Scheduler::pop_raw_() {
+  std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
+  this->items_.pop_back();
+}
+void HOT Scheduler::push_(std::unique_ptr<Scheduler::SchedulerItem> item) { this->to_add_.push_back(std::move(item)); }
+bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
+  bool ret = false;
+  for (auto &it : this->items_)
+    if (it->component == component && it->name == name && it->type == type && !it->remove) {
+      to_remove_++;
+      it->remove = true;
+      ret = true;
+    }
+  for (auto &it : this->to_add_)
+    if (it->component == component && it->name == name && it->type == type) {
+      it->remove = true;
+      ret = true;
+    }
+
+  return ret;
+}
+uint32_t Scheduler::millis_() {
+  const uint32_t now = millis();
+  if (now < this->last_millis_) {
+    ESP_LOGD(TAG, "Incrementing scheduler major");
+    this->millis_major_++;
+  }
+  this->last_millis_ = now;
+  return now;
+}
+
+bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
+                                       const std::unique_ptr<SchedulerItem> &b) {
+  // min-heap
+  // return true if *a* will happen after *b*
+  uint32_t a_next_exec = a->next_execution();
+  uint8_t a_next_exec_major = a->next_execution_major();
+  uint32_t b_next_exec = b->next_execution();
+  uint8_t b_next_exec_major = b->next_execution_major();
+
+  if (a_next_exec_major != b_next_exec_major) {
+    // The "major" calculation is quite complicated.
+    // Basically, we need to check if the major value lies in the future or
+    //
+
+    // Here are some cases to think about:
+    // Format: a_major,b_major -> expected result (a-b, b-a)
+    // a=255,b=0 -> false (255, 1)
+    // a=0,b=1 -> false   (255, 1)
+    // a=1,b=0 -> true    (1, 255)
+    // a=0,b=255 -> true  (1, 255)
+
+    uint8_t diff1 = a_next_exec_major - b_next_exec_major;
+    uint8_t diff2 = b_next_exec_major - a_next_exec_major;
+    return diff1 < diff2;
+  }
+
+  return a_next_exec > b_next_exec;
+}
+
+}  // namespace esphome

+ 67 - 0
livingroom/src/esphome/core/scheduler.h

@@ -0,0 +1,67 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include <vector>
+#include <memory>
+
+namespace esphome {
+
+class Component;
+
+class Scheduler {
+ public:
+  void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> &&func);
+  bool cancel_timeout(Component *component, const std::string &name);
+  void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> &&func);
+  bool cancel_interval(Component *component, const std::string &name);
+
+  optional<uint32_t> next_schedule_in();
+
+  void call();
+
+  void process_to_add();
+
+ protected:
+  struct SchedulerItem {
+    Component *component;
+    std::string name;
+    enum Type { TIMEOUT, INTERVAL } type;
+    union {
+      uint32_t interval;
+      uint32_t timeout;
+    };
+    uint32_t last_execution;
+    std::function<void()> f;
+    bool remove;
+    uint8_t last_execution_major;
+
+    inline uint32_t next_execution() { return this->last_execution + this->timeout; }
+    inline uint8_t next_execution_major() {
+      uint32_t next_exec = this->next_execution();
+      uint8_t next_exec_major = this->last_execution_major;
+      if (next_exec < this->last_execution)
+        next_exec_major++;
+      return next_exec_major;
+    }
+
+    static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
+  };
+
+  uint32_t millis_();
+  void cleanup_();
+  void pop_raw_();
+  void push_(std::unique_ptr<SchedulerItem> item);
+  bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type);
+  bool empty_() {
+    this->cleanup_();
+    return this->items_.empty();
+  }
+
+  std::vector<std::unique_ptr<SchedulerItem>> items_;
+  std::vector<std::unique_ptr<SchedulerItem>> to_add_;
+  uint32_t last_millis_{0};
+  uint8_t millis_major_{0};
+  uint32_t to_remove_{0};
+};
+
+}  // namespace esphome

+ 102 - 0
livingroom/src/esphome/core/util.cpp

@@ -0,0 +1,102 @@
+#include "esphome/core/util.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/application.h"
+#include "esphome/core/version.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_WIFI
+#include "esphome/components/wifi/wifi_component.h"
+#endif
+
+#ifdef USE_API
+#include "esphome/components/api/api_server.h"
+#endif
+
+#ifdef USE_ETHERNET
+#include "esphome/components/ethernet/ethernet_component.h"
+#endif
+
+#ifdef ARDUINO_ARCH_ESP32
+#include <ESPmDNS.h>
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+#include <ESP8266mDNS.h>
+#endif
+
+namespace esphome {
+
+bool network_is_connected() {
+#ifdef USE_ETHERNET
+  if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected())
+    return true;
+#endif
+
+#ifdef USE_WIFI
+  if (wifi::global_wifi_component != nullptr)
+    return wifi::global_wifi_component->is_connected();
+#endif
+
+  return false;
+}
+
+#ifdef ARDUINO_ARCH_ESP8266
+bool mdns_setup;
+#endif
+
+#ifndef WEBSERVER_PORT
+static const uint8_t WEBSERVER_PORT = 80;
+#endif
+
+#ifdef ARDUINO_ARCH_ESP8266
+void network_setup_mdns(IPAddress address, int interface) {
+  // Latest arduino framework breaks mDNS for AP interface
+  // see https://github.com/esp8266/Arduino/issues/6114
+  if (interface == 1)
+    return;
+  MDNS.begin(App.get_name().c_str(), address);
+  mdns_setup = true;
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+  void network_setup_mdns() {
+    MDNS.begin(App.get_name().c_str());
+#endif
+#ifdef USE_API
+    if (api::global_api_server != nullptr) {
+      MDNS.addService("esphomelib", "tcp", api::global_api_server->get_port());
+      // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version
+      MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION);
+      MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str());
+      MDNS.addServiceTxt("esphomelib", "tcp", "mac", get_mac_address().c_str());
+    } else {
+#endif
+      // Publish "http" service if not using native API nor the webserver component
+      // This is just to have *some* mDNS service so that .local resolution works
+      MDNS.addService("http", "tcp", WEBSERVER_PORT);
+      MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION);
+#ifdef USE_API
+    }
+#endif
+#ifdef USE_PROMETHEUS
+    MDNS.addService("prometheus-http", "tcp", WEBSERVER_PORT);
+#endif
+  }
+  void network_tick_mdns() {
+#ifdef ARDUINO_ARCH_ESP8266
+    if (mdns_setup)
+      MDNS.update();
+#endif
+  }
+
+  std::string network_get_address() {
+#ifdef USE_ETHERNET
+    if (ethernet::global_eth_component != nullptr)
+      return ethernet::global_eth_component->get_use_address();
+#endif
+#ifdef USE_WIFI
+    if (wifi::global_wifi_component != nullptr)
+      return wifi::global_wifi_component->get_use_address();
+#endif
+    return "";
+  }
+
+}  // namespace esphome

+ 22 - 0
livingroom/src/esphome/core/util.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include <string>
+#include "IPAddress.h"
+
+namespace esphome {
+
+/// Return whether the node is connected to the network (through wifi, eth, ...)
+bool network_is_connected();
+/// Get the active network hostname
+std::string network_get_address();
+
+/// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode)
+#ifdef ARDUINO_ARCH_ESP8266
+void network_setup_mdns(IPAddress address, int interface);
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+void network_setup_mdns();
+#endif
+void network_tick_mdns();
+
+}  // namespace esphome

+ 2 - 0
livingroom/src/esphome/core/version.h

@@ -0,0 +1,2 @@
+#pragma once
+#define ESPHOME_VERSION "1.16.2"

+ 419 - 0
livingroom/src/main.cpp

@@ -0,0 +1,419 @@
+// Auto generated code by esphome
+// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========
+#include "esphome.h"
+using namespace esphome;
+using namespace time;
+using namespace text_sensor;
+logger::Logger *logger_logger;
+web_server_base::WebServerBase *web_server_base_webserverbase;
+captive_portal::CaptivePortal *captive_portal_captiveportal;
+wifi::WiFiComponent *wifi_wificomponent;
+ota::OTAComponent *ota_otacomponent;
+api::APIServer *api_apiserver;
+using namespace api;
+web_server::WebServer *web_server_webserver;
+using namespace json;
+homeassistant::HomeassistantTime *esptime;
+sun::Sun *sun_sun;
+sun::SunTextSensor *sun_suntextsensor;
+sun::SunTextSensor *sun_suntextsensor_2;
+version::VersionTextSensor *version_versiontextsensor;
+text_sensor::TextSensorStateTrigger *text_sensor_textsensorstatetrigger;
+Automation<std::string> *automation_3;
+template_::TemplateTextSensor *template__templatetextsensor;
+template_::TemplateTextSensor *sunrise_shift;
+template_::TemplateTextSensor *sunset_shift;
+template_::TemplateTextSensor *template__templatetextsensor_2;
+LambdaAction<std::string> *lambdaaction_3;
+sun::SunTrigger *sun_suntrigger;
+Automation<> *automation;
+LambdaAction<> *lambdaaction;
+sun::SunTrigger *sun_suntrigger_2;
+Automation<> *automation_2;
+LambdaAction<> *lambdaaction_2;
+#include "MySunTextSensor.h"
+// ========== AUTO GENERATED INCLUDE BLOCK END ==========="
+
+void setup() {
+  // ===== DO NOT EDIT ANYTHING BELOW THIS LINE =====
+  // ========== AUTO GENERATED CODE BEGIN ===========
+  // async_tcp:
+  // esphome:
+  //   name: livingroom
+  //   platform: ESP8266
+  //   board: esp01_1m
+  //   includes:
+  //   - MySunTextSensor.h
+  //   arduino_version: espressif8266@2.6.2
+  //   build_path: livingroom
+  //   platformio_options: {}
+  //   esp8266_restore_from_flash: false
+  //   board_flash_mode: dout
+  //   libraries: []
+  App.pre_setup("livingroom", __DATE__ ", " __TIME__);
+  // time:
+  // text_sensor:
+  // logger:
+  //   id: logger_logger
+  //   baud_rate: 115200
+  //   tx_buffer_size: 512
+  //   hardware_uart: UART0
+  //   level: DEBUG
+  //   logs: {}
+  //   esp8266_store_log_strings_in_flash: true
+  logger_logger = new logger::Logger(115200, 512, logger::UART_SELECTION_UART0);
+  logger_logger->pre_setup();
+  App.register_component(logger_logger);
+  // web_server_base:
+  //   id: web_server_base_webserverbase
+  web_server_base_webserverbase = new web_server_base::WebServerBase();
+  App.register_component(web_server_base_webserverbase);
+  // captive_portal:
+  //   id: captive_portal_captiveportal
+  //   web_server_base_id: web_server_base_webserverbase
+  captive_portal_captiveportal = new captive_portal::CaptivePortal(web_server_base_webserverbase);
+  App.register_component(captive_portal_captiveportal);
+  // wifi:
+  //   ap:
+  //     ssid: Livingroom Fallback Hotspot
+  //     password: R8ZKoXbtquSv
+  //     id: wifi_wifiap
+  //     ap_timeout: 1min
+  //   id: wifi_wificomponent
+  //   domain: .local
+  //   reboot_timeout: 15min
+  //   power_save_mode: NONE
+  //   fast_connect: false
+  //   output_power: 20.0
+  //   networks:
+  //   - ssid: WLAN-LX
+  //     password: SP-RAXX749x7
+  //     id: wifi_wifiap_2
+  //     priority: 0.0
+  //   use_address: livingroom.local
+  wifi_wificomponent = new wifi::WiFiComponent();
+  wifi_wificomponent->set_use_address("livingroom.local");
+  wifi::WiFiAP wifi_wifiap_2 = wifi::WiFiAP();
+  wifi_wifiap_2.set_ssid("WLAN-LX");
+  wifi_wifiap_2.set_password("SP-RAXX749x7");
+  wifi_wifiap_2.set_priority(0.0f);
+  wifi_wificomponent->add_sta(wifi_wifiap_2);
+  wifi::WiFiAP wifi_wifiap = wifi::WiFiAP();
+  wifi_wifiap.set_ssid("Livingroom Fallback Hotspot");
+  wifi_wifiap.set_password("R8ZKoXbtquSv");
+  wifi_wificomponent->set_ap(wifi_wifiap);
+  wifi_wificomponent->set_ap_timeout(60000);
+  wifi_wificomponent->set_reboot_timeout(900000);
+  wifi_wificomponent->set_power_save_mode(wifi::WIFI_POWER_SAVE_NONE);
+  wifi_wificomponent->set_fast_connect(false);
+  wifi_wificomponent->set_output_power(20.0f);
+  App.register_component(wifi_wificomponent);
+  // ota:
+  //   id: ota_otacomponent
+  //   safe_mode: true
+  //   port: 8266
+  //   password: ''
+  //   reboot_timeout: 5min
+  //   num_attempts: 10
+  ota_otacomponent = new ota::OTAComponent();
+  ota_otacomponent->set_port(8266);
+  ota_otacomponent->set_auth_password("");
+  App.register_component(ota_otacomponent);
+  if (ota_otacomponent->should_enter_safe_mode(10, 300000)) return;
+  // api:
+  //   id: api_apiserver
+  //   port: 6053
+  //   password: ''
+  //   reboot_timeout: 15min
+  api_apiserver = new api::APIServer();
+  App.register_component(api_apiserver);
+  // web_server:
+  //   port: 80
+  //   id: web_server_webserver
+  //   css_url: https:esphome.io/_static/webserver-v1.min.css
+  //   js_url: https:esphome.io/_static/webserver-v1.min.js
+  //   web_server_base_id: web_server_base_webserverbase
+  api_apiserver->set_port(6053);
+  api_apiserver->set_password("");
+  api_apiserver->set_reboot_timeout(900000);
+  web_server_webserver = new web_server::WebServer(web_server_base_webserverbase);
+  App.register_component(web_server_webserver);
+  web_server_base_webserverbase->set_port(80);
+  web_server_webserver->set_css_url("https://esphome.io/_static/webserver-v1.min.css");
+  web_server_webserver->set_js_url("https://esphome.io/_static/webserver-v1.min.js");
+  // json:
+  // substitutions:
+  //   shift_sunset: '+17'
+  //   shift_sunrise: '+6'
+  // time.homeassistant:
+  //   platform: homeassistant
+  //   timezone: CET-1CEST-2,M3.4.0/2,M10.5.0/3
+  //   id: esptime
+  //   update_interval: 15min
+  esptime = new homeassistant::HomeassistantTime();
+  esptime->set_timezone("CET-1CEST-2,M3.4.0/2,M10.5.0/3");
+  // sun:
+  //   latitude: 48.77034
+  //   longitude: 8.34306
+  //   on_sunrise:
+  //   - then:
+  //     - logger.log:
+  //         format: Good morning!
+  //         args: []
+  //         level: DEBUG
+  //         tag: main
+  //       type_id: lambdaaction
+  //     automation_id: automation
+  //     trigger_id: sun_suntrigger
+  //     elevation: -0.883
+  //   on_sunset:
+  //   - then:
+  //     - logger.log:
+  //         format: Good evening!
+  //         args: []
+  //         level: DEBUG
+  //         tag: main
+  //       type_id: lambdaaction_2
+  //     automation_id: automation_2
+  //     trigger_id: sun_suntrigger_2
+  //     elevation: -0.883
+  //   id: sun_sun
+  //   time_id: esptime
+  sun_sun = new sun::Sun();
+  // text_sensor.sun:
+  //   platform: sun
+  //   name: Sun Next Sunrise
+  //   type: sunrise
+  //   id: sun_suntextsensor
+  //   sun_id: sun_sun
+  //   elevation: -0.883
+  //   format: '%X'
+  //   update_interval: 60s
+  //   icon: mdi:weather-sunset-up
+  sun_suntextsensor = new sun::SunTextSensor();
+  sun_suntextsensor->set_update_interval(60000);
+  App.register_component(sun_suntextsensor);
+  // text_sensor.sun:
+  //   platform: sun
+  //   name: Sun Next Sunset
+  //   type: sunset
+  //   id: sun_suntextsensor_2
+  //   sun_id: sun_sun
+  //   elevation: -0.883
+  //   format: '%X'
+  //   update_interval: 60s
+  //   icon: mdi:weather-sunset-down
+  sun_suntextsensor_2 = new sun::SunTextSensor();
+  sun_suntextsensor_2->set_update_interval(60000);
+  App.register_component(sun_suntextsensor_2);
+  // text_sensor.version:
+  //   platform: version
+  //   name: Platform
+  //   on_value:
+  //   - then:
+  //     - lambda: !lambda "ESP_LOGD(\"main\", \"The current version is %s\", x.c_str()); "
+  //       type_id: lambdaaction_3
+  //     automation_id: automation_3
+  //     trigger_id: text_sensor_textsensorstatetrigger
+  //   id: version_versiontextsensor
+  //   icon: mdi:new-box
+  //   hide_timestamp: false
+  version_versiontextsensor = new version::VersionTextSensor();
+  App.register_text_sensor(version_versiontextsensor);
+  version_versiontextsensor->set_name("Platform");
+  version_versiontextsensor->set_icon("mdi:new-box");
+  text_sensor_textsensorstatetrigger = new text_sensor::TextSensorStateTrigger(version_versiontextsensor);
+  automation_3 = new Automation<std::string>(text_sensor_textsensorstatetrigger);
+  // text_sensor.template:
+  //   platform: template
+  //   name: Template Text Sensor
+  //   lambda: !lambda |-
+  //     char str[17];
+  //     time_t currTime = id(esptime).now().timestamp ;
+  //     strftime(str, sizeof(str), "%Y-%m-%d %H:%M", localtime(&currTime));
+  //     return {str};
+  //   update_interval: 60s
+  //   id: template__templatetextsensor
+  template__templatetextsensor = new template_::TemplateTextSensor();
+  template__templatetextsensor->set_update_interval(60000);
+  App.register_component(template__templatetextsensor);
+  // text_sensor.template:
+  //   platform: template
+  //   name: Sunrise
+  //   id: sunrise_shift
+  //   lambda: !lambda |-
+  //     auto sunrise = id(sun_sun).sunrise();
+  //     auto time = id(esptime).now();
+  //     sun_sun->set_latitude(48.77034);
+  //     sun_sun->set_longitude(8.34306);
+  //     if (!sunrise.has_value())
+  //       return {"NaN"};
+  //     int sunrise_min = sunrise.value().hour * 60 + sunrise.value().minute + +6 ;
+  //     int sunrise_hour = int(sunrise_min / 60);
+  //     int sunrise_minute = sunrise_min % 60;
+  //     ESP_LOGD("sunrise_shift", "sunrise.value.()hour is %i , sunrise.value().minute is %i ", sunrise.value().hour , sunrise.value().minute);
+  //     ESP_LOGD("sunrise_shift", "sunrise_min is %i , sunrise_hour is %i ,sunrise_minute is %i ", sunrise_min , sunrise_hour , sunrise_minute);
+  //     char buffer[6];
+  //     sprintf(buffer,"%02i:%02i", sunrise_hour , sunrise_minute);
+  //     return {buffer};
+  //   update_interval: 60s
+  sunrise_shift = new template_::TemplateTextSensor();
+  sunrise_shift->set_update_interval(60000);
+  App.register_component(sunrise_shift);
+  // text_sensor.template:
+  //   platform: template
+  //   name: Sunset
+  //   id: sunset_shift
+  //   lambda: !lambda |-
+  //     auto sunset = id(sun_sun).sunset();
+  //     auto time = id(esptime).now();
+  //     if (!sunset.has_value())
+  //       return {"NaN"};
+  //     int sunset_min = sunset.value().hour * 60 + sunset.value().minute + +17;
+  //     int sunset_hour = int(sunset_min / 60);
+  //     int sunset_minute = sunset_min % 60;
+  //     ESP_LOGD("sunset_shift", "sunset.value.()hour is %i , sunset.value().minute is %i ", sunset.value().hour , sunset.value().minute);
+  //     ESP_LOGD("sunset_shift", "sunset_min is %i , sunset_hour is %i ,sunset_minute is %i ", sunset_min , sunset_hour , sunset_minute);
+  //     char buffer[6];
+  //     sprintf(buffer,"%02i:%02i", sunset_hour , sunset_minute);
+  //     return {buffer};
+  //   update_interval: 60s
+  sunset_shift = new template_::TemplateTextSensor();
+  sunset_shift->set_update_interval(60000);
+  App.register_component(sunset_shift);
+  // text_sensor.template:
+  //   platform: template
+  //   name: Sunset Algo
+  //   lambda: !lambda |-
+  //     int sunrise;
+  //     int sunset;
+  //     auto sunSensor = new MySunTextSensor();
+  //     App.register_component(sunSensor);
+  //     auto sun_sensor = sunSensor->sun;
+  //     auto time = id(esptime).now();
+  //     sun_sensor->setPosition(48.77034, 8.34306, 1);
+  //     sun_sensor->setCurrentDate(2021, 03, 05);
+  //     sunrise = static_cast<int>(sun_sensor->calcSunrise());
+  //     sunset = static_cast<int>(sun_sensor->calcSunset());
+  //     char buffer[100];
+  //   
+  //     sprintf(buffer, "Sunrise at %.2d:%.2dam, Sunset at %.2d:%.2dpm", (sunrise/60), (sunrise%60), (sunset/60), (sunset%60));
+  //     return {buffer};
+  //   id: template__templatetextsensor_2
+  //   update_interval: 60s
+  template__templatetextsensor_2 = new template_::TemplateTextSensor();
+  template__templatetextsensor_2->set_update_interval(60000);
+  App.register_component(template__templatetextsensor_2);
+  esptime->set_update_interval(900000);
+  App.register_component(esptime);
+  App.register_text_sensor(sun_suntextsensor);
+  sun_suntextsensor->set_name("Sun Next Sunrise");
+  sun_suntextsensor->set_icon("mdi:weather-sunset-up");
+  App.register_text_sensor(sun_suntextsensor_2);
+  sun_suntextsensor_2->set_name("Sun Next Sunset");
+  sun_suntextsensor_2->set_icon("mdi:weather-sunset-down");
+  lambdaaction_3 = new LambdaAction<std::string>([=](std::string x) -> void {
+      ESP_LOGD("main", "The current version is %s", x.c_str()); 
+  });
+  App.register_text_sensor(template__templatetextsensor);
+  template__templatetextsensor->set_name("Template Text Sensor");
+  App.register_text_sensor(sunrise_shift);
+  sunrise_shift->set_name("Sunrise");
+  App.register_text_sensor(sunset_shift);
+  sunset_shift->set_name("Sunset");
+  App.register_text_sensor(template__templatetextsensor_2);
+  template__templatetextsensor_2->set_name("Sunset Algo");
+  sun_sun->set_time(esptime);
+  sun_sun->set_latitude(48.77034f);
+  sun_sun->set_longitude(8.34306f);
+  sun_suntrigger = new sun::SunTrigger();
+  App.register_component(sun_suntrigger);
+  sun_suntrigger->set_parent(sun_sun);
+  sun_suntrigger->set_sunrise(true);
+  sun_suntrigger->set_elevation(-0.883f);
+  automation = new Automation<>(sun_suntrigger);
+  sun_suntextsensor->set_parent(sun_sun);
+  sun_suntextsensor->set_sunrise(true);
+  sun_suntextsensor->set_elevation(-0.883f);
+  sun_suntextsensor->set_format("%X");
+  sun_suntextsensor_2->set_parent(sun_sun);
+  sun_suntextsensor_2->set_sunrise(false);
+  sun_suntextsensor_2->set_elevation(-0.883f);
+  sun_suntextsensor_2->set_format("%X");
+  automation_3->add_actions({lambdaaction_3});
+  template__templatetextsensor->set_template([=]() -> optional<std::string> {
+      char str[17];
+      time_t currTime = esptime->now().timestamp ;
+      strftime(str, sizeof(str), "%Y-%m-%d %H:%M", localtime(&currTime));
+      return {str};
+  });
+  template__templatetextsensor_2->set_template([=]() -> optional<std::string> {
+      int sunrise;
+      int sunset;
+      auto sunSensor = new MySunTextSensor();
+      App.register_component(sunSensor);
+      auto sun_sensor = sunSensor->sun;
+      auto time = esptime->now();
+      sun_sensor->setPosition(48.77034, 8.34306, 1);
+      sun_sensor->setCurrentDate(2021, 03, 05);
+      sunrise = static_cast<int>(sun_sensor->calcSunrise());
+      sunset = static_cast<int>(sun_sensor->calcSunset());
+      char buffer[100];
+      
+      sprintf(buffer, "Sunrise at %.2d:%.2dam, Sunset at %.2d:%.2dpm", (sunrise/60), (sunrise%60), (sunset/60), (sunset%60));
+      return {buffer};
+  });
+  lambdaaction = new LambdaAction<>([=]() -> void {
+      ESP_LOGD("main", "Good morning!");
+  });
+  sunrise_shift->set_template([=]() -> optional<std::string> {
+      auto sunrise = sun_sun->sunrise();
+      auto time = esptime->now();
+      sun_sun->set_latitude(48.77034);
+      sun_sun->set_longitude(8.34306);
+      if (!sunrise.has_value())
+        return {"NaN"};
+      int sunrise_min = sunrise.value().hour * 60 + sunrise.value().minute + +6 ;
+      int sunrise_hour = int(sunrise_min / 60);
+      int sunrise_minute = sunrise_min % 60;
+      ESP_LOGD("sunrise_shift", "sunrise.value.()hour is %i , sunrise.value().minute is %i ", sunrise.value().hour , sunrise.value().minute);
+      ESP_LOGD("sunrise_shift", "sunrise_min is %i , sunrise_hour is %i ,sunrise_minute is %i ", sunrise_min , sunrise_hour , sunrise_minute);
+      char buffer[6];
+      sprintf(buffer,"%02i:%02i", sunrise_hour , sunrise_minute);
+      return {buffer};
+  });
+  sunset_shift->set_template([=]() -> optional<std::string> {
+      auto sunset = sun_sun->sunset();
+      auto time = esptime->now();
+      if (!sunset.has_value())
+        return {"NaN"};
+      int sunset_min = sunset.value().hour * 60 + sunset.value().minute + +17;
+      int sunset_hour = int(sunset_min / 60);
+      int sunset_minute = sunset_min % 60;
+      ESP_LOGD("sunset_shift", "sunset.value.()hour is %i , sunset.value().minute is %i ", sunset.value().hour , sunset.value().minute);
+      ESP_LOGD("sunset_shift", "sunset_min is %i , sunset_hour is %i ,sunset_minute is %i ", sunset_min , sunset_hour , sunset_minute);
+      char buffer[6];
+      sprintf(buffer,"%02i:%02i", sunset_hour , sunset_minute);
+      return {buffer};
+  });
+  App.register_component(version_versiontextsensor);
+  automation->add_actions({lambdaaction});
+  version_versiontextsensor->set_hide_timestamp(false);
+  sun_suntrigger_2 = new sun::SunTrigger();
+  App.register_component(sun_suntrigger_2);
+  sun_suntrigger_2->set_parent(sun_sun);
+  sun_suntrigger_2->set_sunrise(false);
+  sun_suntrigger_2->set_elevation(-0.883f);
+  automation_2 = new Automation<>(sun_suntrigger_2);
+  lambdaaction_2 = new LambdaAction<>([=]() -> void {
+      ESP_LOGD("main", "Good evening!");
+  });
+  automation_2->add_actions({lambdaaction_2});
+  // =========== AUTO GENERATED CODE END ============
+  // ========= YOU CAN EDIT AFTER THIS LINE =========
+  App.setup();
+}
+
+void loop() {
+  App.loop();
+}

+ 587 - 0
livingroom/src/sunset.cpp

@@ -0,0 +1,587 @@
+/*
+ * Provides the ability to calculate the local time for sunrise,
+ * sunset, and moonrise at any point in time at any location in the world
+ *
+ * Original work used with permission maintaining license
+ * Copyright (GPL) 2004 Mike Chirico mchirico@comcast.net
+ * Modifications copyright
+ * Copyright (GPL) 2015 Peter Buelow
+ *
+ * This file is part of the Sunset library
+ *
+ * Sunset is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Sunset is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "sunset.h"
+
+/**
+ * \fn SunSet::SunSet()
+ *
+ * Default constructor taking no arguments. It will default all values
+ * to zero. It is possible to call calcSunrise() on this type of initialization
+ * and it will not fail, but it is unlikely you are at 0,0, TZ=0. This also
+ * will not include an initialized date to work from.
+ */
+SunSet::SunSet() : m_latitude(0.0), m_longitude(0.0), m_julianDate(0.0), m_tzOffset(0.0)
+{
+}
+
+/**
+ * \fn SunSet::SunSet(double lat, double lon, int tz)
+ * \param lat Double Latitude for this object
+ * \param lon Double Longitude for this object
+ * \param tz Integer based timezone for this object
+ *
+ * This will create an object for a location with an integer based
+ * timezone value. This constructor is a relic of the original design. 
+ * It is not deprecated, as this is a valid construction, but the double is
+ * preferred for correctness.
+ */
+SunSet::SunSet(double lat, double lon, int tz) : m_latitude(lat), m_longitude(lon), m_julianDate(0.0), m_tzOffset(tz)
+{
+}
+
+/**
+ * \fn SunSet::SunSet(double lat, double lon, double tz)
+ * \param lat Double Latitude for this object
+ * \param lon Double Longitude for this object
+ * \param tz Double based timezone for this object
+ *
+ * This will create an object for a location with a double based
+ * timezone value.
+ */
+SunSet::SunSet(double lat, double lon, double tz) : m_latitude(lat), m_longitude(lon), m_julianDate(0.0), m_tzOffset(tz)
+{
+}
+
+/**
+ * \fn SunSet::~SunSet()
+ *
+ * The constructor has no value and does nothing.
+ */
+SunSet::~SunSet()
+{
+}
+
+/**
+ * \fn void SunSet::setPosition(double lat, double lon, int tz)
+ * \param lat Double Latitude value
+ * \param lon Double Longitude value
+ * \param tz Integer Timezone offset
+ *
+ * This will set the location the library uses for it's math. The
+ * timezone is included in this as it's not valid to call
+ * any of the calc functions until you have set a timezone.
+ * It is possible to simply call setPosition one time, with a timezone
+ * and not use the setTZOffset() function ever, if you never
+ * change timezone values.
+ * 
+ * This is the old version of the setPosition using an integer
+ * timezone, and will not be deprecated. However, it is preferred to
+ * use the double version going forward.
+ */
+void SunSet::setPosition(double lat, double lon, int tz)
+{
+    m_latitude = lat;
+    m_longitude = lon;
+    if (tz >= -12 && tz <= 14)
+        m_tzOffset = tz;
+    else
+        m_tzOffset = 0.0;
+}
+
+/**
+ * \fn void SunSet::setPosition(double lat, double lon, double tz)
+ * \param lat Double Latitude value
+ * \param lon Double Longitude value
+ * \param tz Double Timezone offset
+ *
+ * This will set the location the library uses for it's math. The
+ * timezone is included in this as it's not valid to call
+ * any of the calc functions until you have set a timezone.
+ * It is possible to simply call setPosition one time, with a timezone
+ * and not use the setTZOffset() function ever, if you never
+ * change timezone values.
+ */
+void SunSet::setPosition(double lat, double lon, double tz)
+{
+    m_latitude = lat;
+    m_longitude = lon;
+    if (tz >= -12 && tz <= 14)
+        m_tzOffset = tz;
+    else
+        m_tzOffset = 0.0;
+}
+
+double SunSet::degToRad(double angleDeg) const
+{
+    return (M_PI * angleDeg / 180.0);
+}
+
+double SunSet::radToDeg(double angleRad) const
+{
+    return (180.0 * angleRad / M_PI);
+}
+
+double SunSet::calcMeanObliquityOfEcliptic(double t) const
+{
+    double seconds = 21.448 - t*(46.8150 + t*(0.00059 - t*(0.001813)));
+    double e0 = 23.0 + (26.0 + (seconds/60.0))/60.0;
+
+    return e0;              // in degrees
+}
+
+double SunSet::calcGeomMeanLongSun(double t) const
+{
+    if (std::isnan(t)) {
+        return nan("");
+    }
+    double L = 280.46646 + t * (36000.76983 + 0.0003032 * t);
+
+    return std::fmod(L, 360.0);
+}
+
+double SunSet::calcObliquityCorrection(double t) const
+{
+    double e0 = calcMeanObliquityOfEcliptic(t);
+    double omega = 125.04 - 1934.136 * t;
+    double e = e0 + 0.00256 * cos(degToRad(omega));
+
+    return e;               // in degrees
+}
+
+double SunSet::calcEccentricityEarthOrbit(double t) const
+{
+    double e = 0.016708634 - t * (0.000042037 + 0.0000001267 * t);
+    return e;               // unitless
+}
+
+double SunSet::calcGeomMeanAnomalySun(double t) const
+{
+    double M = 357.52911 + t * (35999.05029 - 0.0001537 * t);
+    return M;               // in degrees
+}
+
+double SunSet::calcEquationOfTime(double t) const
+{
+    double epsilon = calcObliquityCorrection(t);
+    double l0 = calcGeomMeanLongSun(t);
+    double e = calcEccentricityEarthOrbit(t);
+    double m = calcGeomMeanAnomalySun(t);
+    double y = tan(degToRad(epsilon)/2.0);
+
+    y *= y;
+
+    double sin2l0 = sin(2.0 * degToRad(l0));
+    double sinm   = sin(degToRad(m));
+    double cos2l0 = cos(2.0 * degToRad(l0));
+    double sin4l0 = sin(4.0 * degToRad(l0));
+    double sin2m  = sin(2.0 * degToRad(m));
+    double Etime = y * sin2l0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2l0 - 0.5 * y * y * sin4l0 - 1.25 * e * e * sin2m;
+    return radToDeg(Etime)*4.0;	// in minutes of time
+}
+
+double SunSet::calcTimeJulianCent(double jd) const
+{
+    double T = ( jd - 2451545.0)/36525.0;
+    return T;
+}
+
+double SunSet::calcSunTrueLong(double t) const
+{
+    double l0 = calcGeomMeanLongSun(t);
+    double c = calcSunEqOfCenter(t);
+
+    double O = l0 + c;
+    return O;               // in degrees
+}
+
+double SunSet::calcSunApparentLong(double t) const
+{
+    double o = calcSunTrueLong(t);
+
+    double  omega = 125.04 - 1934.136 * t;
+    double  lambda = o - 0.00569 - 0.00478 * sin(degToRad(omega));
+    return lambda;          // in degrees
+}
+
+double SunSet::calcSunDeclination(double t) const
+{
+    double e = calcObliquityCorrection(t);
+    double lambda = calcSunApparentLong(t);
+
+    double sint = sin(degToRad(e)) * sin(degToRad(lambda));
+    double theta = radToDeg(asin(sint));
+    return theta;           // in degrees
+}
+
+double SunSet::calcHourAngleSunrise(double lat, double solarDec, double offset) const
+{
+    double latRad = degToRad(lat);
+    double sdRad  = degToRad(solarDec);
+    double HA = (acos(cos(degToRad(offset))/(cos(latRad)*cos(sdRad))-tan(latRad) * tan(sdRad)));
+
+    return HA;              // in radians
+}
+
+double SunSet::calcHourAngleSunset(double lat, double solarDec, double offset) const
+{
+    double latRad = degToRad(lat);
+    double sdRad  = degToRad(solarDec);
+    double HA = (acos(cos(degToRad(offset))/(cos(latRad)*cos(sdRad))-tan(latRad) * tan(sdRad)));
+
+    return -HA;              // in radians
+}
+
+/**
+ * \fn double SunSet::calcJD(int y, int m, int d) const
+ * \param y Integer year as a 4 digit value
+ * \param m Integer month, not 0 based
+ * \param d Integer day, not 0 based
+ * \return Returns the Julian date as a double for the calculations
+ *
+ * A well known JD calculator
+ */
+double SunSet::calcJD(int y, int m, int d) const
+{
+    if (m <= 2) {
+        y -= 1;
+        m += 12;
+    }
+    double A = floor(y/100);
+    double B = 2.0 - A + floor(A/4);
+
+    double JD = floor(365.25*(y + 4716)) + floor(30.6001*(m+1)) + d + B - 1524.5;
+    return JD;
+}
+
+double SunSet::calcJDFromJulianCent(double t) const
+{
+    double JD = t * 36525.0 + 2451545.0;
+    return JD;
+}
+
+double SunSet::calcSunEqOfCenter(double t) const
+{
+    double m = calcGeomMeanAnomalySun(t);
+    double mrad = degToRad(m);
+    double sinm = sin(mrad);
+    double sin2m = sin(mrad+mrad);
+    double sin3m = sin(mrad+mrad+mrad);
+    double C = sinm * (1.914602 - t * (0.004817 + 0.000014 * t)) + sin2m * (0.019993 - 0.000101 * t) + sin3m * 0.000289;
+
+    return C;		// in degrees
+}
+
+/**
+ * \fn double SunSet::calcAbsSunrise(double offset) const
+ * \param offset Double The specific angle to use when calculating sunrise
+ * \return Returns the time in minutes past midnight in UTC for sunrise at your location
+ *
+ * This does a bunch of work to get to an accurate angle. Note that it does it 2x, once
+ * to get a rough position, and then it doubles back and redoes the calculations to 
+ * refine the value. The first time through, it will be off by as much as 2 minutes, but
+ * the second time through, it will be nearly perfect.
+ *
+ * Note that this is the base calculation for all sunrise calls. The others just modify
+ * the offset angle to account for the different needs.
+ */
+double SunSet::calcAbsSunrise(double offset) const
+{
+    double t = calcTimeJulianCent(m_julianDate);
+    // *** First pass to approximate sunrise
+    double  eqTime = calcEquationOfTime(t);
+    double  solarDec = calcSunDeclination(t);
+    double  hourAngle = calcHourAngleSunrise(m_latitude, solarDec, offset);
+    double  delta = m_longitude + radToDeg(hourAngle);
+    double  timeDiff = 4 * delta;	// in minutes of time
+    double  timeUTC = 720 - timeDiff - eqTime;	// in minutes
+    double  newt = calcTimeJulianCent(calcJDFromJulianCent(t) + timeUTC/1440.0);
+
+    eqTime = calcEquationOfTime(newt);
+    solarDec = calcSunDeclination(newt);
+
+    hourAngle = calcHourAngleSunrise(m_latitude, solarDec, offset);
+    delta = m_longitude + radToDeg(hourAngle);
+    timeDiff = 4 * delta;
+    timeUTC = 720 - timeDiff - eqTime; // in minutes
+    return timeUTC;	// return time in minutes from midnight
+}
+
+/**
+ * \fn double SunSet::calcAbsSunset(double offset) const
+ * \param offset Double The specific angle to use when calculating sunset
+ * \return Returns the time in minutes past midnight in UTC for sunset at your location
+ *
+ * This does a bunch of work to get to an accurate angle. Note that it does it 2x, once
+ * to get a rough position, and then it doubles back and redoes the calculations to 
+ * refine the value. The first time through, it will be off by as much as 2 minutes, but
+ * the second time through, it will be nearly perfect.
+ *
+ * Note that this is the base calculation for all sunset calls. The others just modify
+ * the offset angle to account for the different needs.
+*/
+double SunSet::calcAbsSunset(double offset) const
+{
+    double t = calcTimeJulianCent(m_julianDate);
+    // *** First pass to approximate sunset
+    double  eqTime = calcEquationOfTime(t);
+    double  solarDec = calcSunDeclination(t);
+    double  hourAngle = calcHourAngleSunset(m_latitude, solarDec, offset);
+    double  delta = m_longitude + radToDeg(hourAngle);
+    double  timeDiff = 4 * delta;	// in minutes of time
+    double  timeUTC = 720 - timeDiff - eqTime;	// in minutes
+    double  newt = calcTimeJulianCent(calcJDFromJulianCent(t) + timeUTC/1440.0);
+
+    eqTime = calcEquationOfTime(newt);
+    solarDec = calcSunDeclination(newt);
+
+    hourAngle = calcHourAngleSunset(m_latitude, solarDec, offset);
+    delta = m_longitude + radToDeg(hourAngle);
+    timeDiff = 4 * delta;
+    timeUTC = 720 - timeDiff - eqTime; // in minutes
+
+    return timeUTC;	// return time in minutes from midnight
+}
+
+/**
+ * \fn double SunSet::calcSunriseUTC()
+ * \return Returns the UTC time when sunrise occurs in the location provided
+ *
+ * This is a holdover from the original implementation and to me doesn't
+ * seem to be very useful, it's just confusing. This function is deprecated
+ * but won't be removed unless that becomes necessary.
+ */
+double SunSet::calcSunriseUTC()
+{
+    return calcAbsSunrise(SUNSET_OFFICIAL);
+}
+
+/**
+ * \fn double SunSet::calcSunsetUTC() const
+ * \return Returns the UTC time when sunset occurs in the location provided
+ *
+ * This is a holdover from the original implementation and to me doesn't
+ * seem to be very useful, it's just confusing. This function is deprecated
+ * but won't be removed unless that becomes necessary.
+ */
+double SunSet::calcSunsetUTC()
+{
+    return calcAbsSunset(SUNSET_OFFICIAL);
+}
+
+/**
+ * \fn double SunSet::calcAstronomicalSunrise()
+ * \return Returns the Astronomical sunrise in fractional minutes past midnight
+ *
+ * This function will return the Astronomical sunrise in local time for your location
+ */
+double SunSet::calcAstronomicalSunrise() const
+{
+    return calcCustomSunrise(SUNSET_ASTONOMICAL);
+}
+
+/**
+ * \fn double SunSet::calcAstronomicalSunset() const
+ * \return Returns the Astronomical sunset in fractional minutes past midnight
+ *
+ * This function will return the Astronomical sunset in local time for your location
+ */
+double SunSet::calcAstronomicalSunset() const
+{
+    return calcCustomSunset(SUNSET_ASTONOMICAL);
+}
+
+/**
+ * \fn double SunSet::calcCivilSunrise() const
+ * \return Returns the Civil sunrise in fractional minutes past midnight
+ *
+ * This function will return the Civil sunrise in local time for your location
+ */
+double SunSet::calcCivilSunrise() const
+{
+    return calcCustomSunrise(SUNSET_CIVIL);
+}
+
+/**
+ * \fn double SunSet::calcCivilSunset() const
+ * \return Returns the Civil sunset in fractional minutes past midnight
+ *
+ * This function will return the Civil sunset in local time for your location
+ */
+double SunSet::calcCivilSunset() const
+{
+    return calcCustomSunset(SUNSET_CIVIL);
+}
+
+/**
+ * \fn double SunSet::calcNauticalSunrise() const
+ * \return Returns the Nautical sunrise in fractional minutes past midnight
+ *
+ * This function will return the Nautical sunrise in local time for your location
+ */
+double SunSet::calcNauticalSunrise() const
+{
+    return calcCustomSunrise(SUNSET_NAUTICAL);
+}
+
+/**
+ * \fn double SunSet::calcNauticalSunset() const
+ * \return Returns the Nautical sunset in fractional minutes past midnight
+ *
+ * This function will return the Nautical sunset in local time for your location
+ */
+double SunSet::calcNauticalSunset() const
+{
+    return calcCustomSunset(SUNSET_NAUTICAL);
+}
+
+/**
+ * \fn double SunSet::calcSunrise() const
+ * \return Returns local sunrise in minutes past midnight.
+ *
+ * This function will return the Official sunrise in local time for your location
+ */
+double SunSet::calcSunrise() const
+{
+    return calcCustomSunrise(SUNSET_OFFICIAL);
+}
+
+/**
+ * \fn double SunSet::calcSunset() const
+ * \return Returns local sunset in minutes past midnight.
+ *
+ * This function will return the Official sunset in local time for your location
+ */
+double SunSet::calcSunset() const
+{
+    return calcCustomSunset(SUNSET_OFFICIAL);
+}
+
+/**
+ * \fn double SunSet::calcCustomSunrise(double angle) const
+ * \param angle The angle in degrees over the horizon at which to calculate the sunset time
+ * \return Returns sunrise at angle degrees in minutes past midnight.
+ *
+ * This function will return the sunrise in local time for your location for any
+ * angle over the horizon, where < 90 would be above the horizon, and > 90 would be at or below.
+ */
+double SunSet::calcCustomSunrise(double angle) const
+{
+    return calcAbsSunrise(angle) + (60 * m_tzOffset);
+}
+
+/**
+ * \fn double SunSet::calcCustomSunset(double angle) const
+ * \param angle The angle in degrees over the horizon at which to calculate the sunset time
+ * \return Returns sunset at angle degrees in minutes past midnight.
+ *
+ * This function will return the sunset in local time for your location for any
+ * angle over the horizon, where < 90 would be above the horizon, and > 90 would be at or below.
+ */
+double SunSet::calcCustomSunset(double angle) const
+{
+    return calcAbsSunset(angle) + (60 * m_tzOffset);
+}
+
+/**
+ * double SunSet::setCurrentDate(int y, int m, int d)
+ * \param y Integer year, must be 4 digits
+ * \param m Integer month, not zero based (Jan = 1)
+ * \param d Integer day of month, not zero based (month starts on day 1)
+ * \return Returns the result of the Julian Date conversion if you want to save it
+ *
+ * Since these calculations are done based on the Julian Calendar, we must convert
+ * our year month day into Julian before we use it. You get the Julian value for
+ * free if you want it.
+ */
+double SunSet::setCurrentDate(int y, int m, int d)
+{
+	m_year = y;
+	m_month = m;
+	m_day = d;
+	m_julianDate = calcJD(y, m, d);
+	return m_julianDate;
+}
+
+/**
+ * \fn void SunSet::setTZOffset(int tz)
+ * \param tz Integer timezone, may be positive or negative
+ *
+ * Critical to set your timezone so results are accurate for your time and date.
+ * This function is critical to make sure the system works correctly. If you
+ * do not set the timezone correctly, the return value will not be correct for
+ * your location. Forgetting this will result in return values that may actually
+ * be negative in some cases.
+ *
+ * This function is a holdover from the previous design using an integer timezone
+ * and will not be deprecated. It is preferred to use the setTZOffset(doubble).
+ */
+void SunSet::setTZOffset(int tz)
+{
+    if (tz >= -12 && tz <= 14)
+        m_tzOffset = static_cast<double>(tz);
+    else
+        m_tzOffset = 0.0;
+}
+
+/**
+ * \fn void SunSet::setTZOffset(double tz)
+ * \param tz Double timezone, may be positive or negative
+ *
+ * Critical to set your timezone so results are accurate for your time and date.
+ * This function is critical to make sure the system works correctly. If you
+ * do not set the timezone correctly, the return value will not be correct for
+ * your location. Forgetting this will result in return values that may actually
+ * be negative in some cases.
+ */
+void SunSet::setTZOffset(double tz)
+{
+    if (tz >= -12 && tz <= 14)
+        m_tzOffset = tz;
+    else
+        m_tzOffset = 0.0;
+}
+
+/**
+ * \fn int SunSet::moonPhase(int fromepoch) const
+ * \param fromepoch time_t seconds from epoch to calculate the moonphase for
+ *
+ * This is a simple calculation to tell us roughly what the moon phase is
+ * locally. It does not give position. It's roughly accurate, but not great.
+ *
+ * The return value is 0 to 29, with 0 and 29 being hidden and 14 being full.
+ */
+int SunSet::moonPhase(int fromepoch) const
+{
+	int moonepoch = 614100;
+    int phase = (fromepoch - moonepoch) % 2551443;
+    int res = static_cast<int>(floor(phase / (24 * 3600))) + 1;
+
+    if (res == 30)
+        res = 0;
+
+    return res;
+}
+
+/**
+ * \fn int SunSet::moonPhase() const
+ *
+ * Overload to set the moonphase for right now
+ */
+int SunSet::moonPhase() const
+{
+    time_t t = std::time(0);
+    return moonPhase(static_cast<int>(t));
+}
+

+ 129 - 0
livingroom/src/sunset.h

@@ -0,0 +1,129 @@
+/*
+ * Provides the ability to calculate the local time for sunrise,
+ * sunset, and moonrise at any point in time at any location in the world
+ *
+ * Original work used with permission maintaining license
+ * Copyright (GPL) 2004 Mike Chirico mchirico@comcast.net
+ * Modifications copyright
+ * Copyright (GPL) 2015 Peter Buelow
+ *
+ * This file is part of the Sunset library
+ *
+ * Sunset is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Sunset is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Foobar.    If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SUNPOSITION_H__
+#define __SUNPOSITION_H__
+
+#include <cmath>
+#include <ctime>
+
+#ifndef M_PI
+  #define M_PI 3.14159265358979323846264338327950288
+#endif
+
+/**
+ * \class SunSet
+ *
+ * This class controls all aspects of the operations. The math is done in private
+ * functions, and the public API's allow for returning a sunrise/sunset value for the
+ * given coordinates and timezone.
+ *
+ * The resulting calculations are relative to midnight of the day you set in the
+ * setCurrentDate() function. It does not return a time_t value for delta from the
+ * current epoch as that would not make sense as the sunrise/sunset can be calculated
+ * thousands of years in the past. The library acts on a day timeframe, and doesn't
+ * try to assume anything about any other unit of measurement other than the current
+ * set day.
+ *
+ * You can instantiate this class a few different ways, depending on your needs. It's possible
+ * to set your location one time and forget about doing that again if you don't plan to move.
+ * Then you only need to change the date and timezone to get correct data. Or, you can
+ * simply create an object with no location or time data and then do that later. This is
+ * a good mechanism for the setup()/loop() construct.
+ *
+ * The most important thing to remember is to make sure the library knows the exact date and
+ * timezone for the sunrise/sunset you are trying to calculate. Not doing so means you are going
+ * to have very odd results. It's reasonably easy to know when you've forgotten one or the other
+ * by looking at the time the sun would rise and noticing that it is X hours earlier or later.
+ * That is, if you get a return of 128 minutes (2:08 AM) past midnight when the sun should rise 
+ * at 308 (6:08 AM), then you probably forgot to set your EST timezone.
+ *
+ * The library also has no idea about daylight savings time. If your timezone changes during the
+ * year to account for savings time, you must update your timezone accordingly.
+ */
+class SunSet {
+public:
+    SunSet();
+    SunSet(double, double, int);
+    SunSet(double, double, double);
+    ~SunSet();
+
+    static constexpr double SUNSET_OFFICIAL = 90.833;       /**< Standard sun angle for sunset */
+    static constexpr double SUNSET_NAUTICAL = 102.0;        /**< Nautical sun angle for sunset */
+    static constexpr double SUNSET_CIVIL = 96.0;            /**< Civil sun angle for sunset */
+    static constexpr double SUNSET_ASTONOMICAL = 108.0;     /**< Astronomical sun angle for sunset */
+
+    void setPosition(double, double, int);
+    void setPosition(double, double, double);
+    void setTZOffset(int);
+    void setTZOffset(double);
+    double setCurrentDate(int, int, int);
+    double calcNauticalSunrise() const;
+    double calcNauticalSunset() const;
+    double calcCivilSunrise() const;
+    double calcCivilSunset() const;
+    double calcAstronomicalSunrise() const;
+    double calcAstronomicalSunset() const;
+    double calcCustomSunrise(double) const;
+    double calcCustomSunset(double) const;
+    [[deprecated("UTC specific calls may not be supported in the future")]] double calcSunriseUTC();
+    [[deprecated("UTC specific calls may not be supported in the future")]] double calcSunsetUTC();
+    double calcSunrise() const;
+    double calcSunset() const;
+    int moonPhase(int) const;
+    int moonPhase() const;
+
+private:
+    double degToRad(double) const;
+    double radToDeg(double) const;
+    double calcMeanObliquityOfEcliptic(double) const;
+    double calcGeomMeanLongSun(double) const;
+    double calcObliquityCorrection(double) const;
+    double calcEccentricityEarthOrbit(double) const;
+    double calcGeomMeanAnomalySun(double) const;
+    double calcEquationOfTime(double) const;
+    double calcTimeJulianCent(double) const;
+    double calcSunTrueLong(double) const;
+    double calcSunApparentLong(double) const;
+    double calcSunDeclination(double) const;
+    double calcHourAngleSunrise(double, double, double) const;
+    double calcHourAngleSunset(double, double, double) const;
+    double calcJD(int,int,int) const;
+    double calcJDFromJulianCent(double) const;
+    double calcSunEqOfCenter(double) const;
+    double calcAbsSunrise(double) const;
+    double calcAbsSunset(double) const;
+
+    double m_latitude;
+    double m_longitude;
+    double m_julianDate;
+    double m_tzOffset;
+    int m_year;
+    int m_month;
+    int m_day;
+};
+
+#endif
+

+ 129 - 0
sunset.h

@@ -0,0 +1,129 @@
+/*
+ * Provides the ability to calculate the local time for sunrise,
+ * sunset, and moonrise at any point in time at any location in the world
+ *
+ * Original work used with permission maintaining license
+ * Copyright (GPL) 2004 Mike Chirico mchirico@comcast.net
+ * Modifications copyright
+ * Copyright (GPL) 2015 Peter Buelow
+ *
+ * This file is part of the Sunset library
+ *
+ * Sunset is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Sunset is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Foobar.    If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SUNPOSITION_H__
+#define __SUNPOSITION_H__
+
+#include <cmath>
+#include <ctime>
+
+#ifndef M_PI
+  #define M_PI 3.14159265358979323846264338327950288
+#endif
+
+/**
+ * \class SunSet
+ *
+ * This class controls all aspects of the operations. The math is done in private
+ * functions, and the public API's allow for returning a sunrise/sunset value for the
+ * given coordinates and timezone.
+ *
+ * The resulting calculations are relative to midnight of the day you set in the
+ * setCurrentDate() function. It does not return a time_t value for delta from the
+ * current epoch as that would not make sense as the sunrise/sunset can be calculated
+ * thousands of years in the past. The library acts on a day timeframe, and doesn't
+ * try to assume anything about any other unit of measurement other than the current
+ * set day.
+ *
+ * You can instantiate this class a few different ways, depending on your needs. It's possible
+ * to set your location one time and forget about doing that again if you don't plan to move.
+ * Then you only need to change the date and timezone to get correct data. Or, you can
+ * simply create an object with no location or time data and then do that later. This is
+ * a good mechanism for the setup()/loop() construct.
+ *
+ * The most important thing to remember is to make sure the library knows the exact date and
+ * timezone for the sunrise/sunset you are trying to calculate. Not doing so means you are going
+ * to have very odd results. It's reasonably easy to know when you've forgotten one or the other
+ * by looking at the time the sun would rise and noticing that it is X hours earlier or later.
+ * That is, if you get a return of 128 minutes (2:08 AM) past midnight when the sun should rise 
+ * at 308 (6:08 AM), then you probably forgot to set your EST timezone.
+ *
+ * The library also has no idea about daylight savings time. If your timezone changes during the
+ * year to account for savings time, you must update your timezone accordingly.
+ */
+class SunSet {
+public:
+    SunSet();
+    SunSet(double, double, int);
+    SunSet(double, double, double);
+    ~SunSet();
+
+    static constexpr double SUNSET_OFFICIAL = 90.833;       /**< Standard sun angle for sunset */
+    static constexpr double SUNSET_NAUTICAL = 102.0;        /**< Nautical sun angle for sunset */
+    static constexpr double SUNSET_CIVIL = 96.0;            /**< Civil sun angle for sunset */
+    static constexpr double SUNSET_ASTONOMICAL = 108.0;     /**< Astronomical sun angle for sunset */
+
+    void setPosition(double, double, int);
+    void setPosition(double, double, double);
+    void setTZOffset(int);
+    void setTZOffset(double);
+    double setCurrentDate(int, int, int);
+    double calcNauticalSunrise() const;
+    double calcNauticalSunset() const;
+    double calcCivilSunrise() const;
+    double calcCivilSunset() const;
+    double calcAstronomicalSunrise() const;
+    double calcAstronomicalSunset() const;
+    double calcCustomSunrise(double) const;
+    double calcCustomSunset(double) const;
+    [[deprecated("UTC specific calls may not be supported in the future")]] double calcSunriseUTC();
+    [[deprecated("UTC specific calls may not be supported in the future")]] double calcSunsetUTC();
+    double calcSunrise() const;
+    double calcSunset() const;
+    int moonPhase(int) const;
+    int moonPhase() const;
+
+private:
+    double degToRad(double) const;
+    double radToDeg(double) const;
+    double calcMeanObliquityOfEcliptic(double) const;
+    double calcGeomMeanLongSun(double) const;
+    double calcObliquityCorrection(double) const;
+    double calcEccentricityEarthOrbit(double) const;
+    double calcGeomMeanAnomalySun(double) const;
+    double calcEquationOfTime(double) const;
+    double calcTimeJulianCent(double) const;
+    double calcSunTrueLong(double) const;
+    double calcSunApparentLong(double) const;
+    double calcSunDeclination(double) const;
+    double calcHourAngleSunrise(double, double, double) const;
+    double calcHourAngleSunset(double, double, double) const;
+    double calcJD(int,int,int) const;
+    double calcJDFromJulianCent(double) const;
+    double calcSunEqOfCenter(double) const;
+    double calcAbsSunrise(double) const;
+    double calcAbsSunset(double) const;
+
+    double m_latitude;
+    double m_longitude;
+    double m_julianDate;
+    double m_tzOffset;
+    int m_year;
+    int m_month;
+    int m_day;
+};
+
+#endif
+