mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			39 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e20ec00071 | ||
| 
						 | 
					150114d774 | ||
| 
						 | 
					89dfa5ea82 | ||
| 
						 | 
					97aa930ad2 | ||
| 
						 | 
					2a5def10e7 | ||
| 
						 | 
					969834e037 | ||
| 
						 | 
					d73a44c504 | ||
| 
						 | 
					8aec092ab6 | ||
| 
						 | 
					4fa959ba45 | ||
| 
						 | 
					b43712d78d | ||
| 
						 | 
					01904a0f10 | ||
| 
						 | 
					dd875e7529 | ||
| 
						 | 
					f1dcf0f0b8 | ||
| 
						 | 
					a045d001bf | ||
| 
						 | 
					066c1022d0 | ||
| 
						 | 
					59c192becc | ||
| 
						 | 
					a800816750 | ||
| 
						 | 
					99d9ab4e40 | ||
| 
						 | 
					f310ca1b74 | ||
| 
						 | 
					f763daa577 | ||
| 
						 | 
					970563e07b | ||
| 
						 | 
					e006045f59 | ||
| 
						 | 
					fff3645901 | ||
| 
						 | 
					a5383fd208 | ||
| 
						 | 
					9ce3a2059f | ||
| 
						 | 
					69e6cf2c0c | ||
| 
						 | 
					28635124f9 | ||
| 
						 | 
					035be87a83 | ||
| 
						 | 
					e8bdbc45a9 | ||
| 
						 | 
					429caccefa | ||
| 
						 | 
					744ca1af7c | ||
| 
						 | 
					106f0d611f | ||
| 
						 | 
					d826416684 | ||
| 
						 | 
					81959804df | ||
| 
						 | 
					75b524ddc4 | ||
| 
						 | 
					f599c36272 | ||
| 
						 | 
					9bb64315f3 | ||
| 
						 | 
					575badc690 | ||
| 
						 | 
					4b91cfb7f9 | 
@@ -36,6 +36,7 @@ esphome/components/dfplayer/* @glmnet
 | 
			
		||||
esphome/components/dht/* @OttoWinter
 | 
			
		||||
esphome/components/ds1307/* @badbadc0ffee
 | 
			
		||||
esphome/components/esp32_ble/* @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_server/* @jesserockz
 | 
			
		||||
esphome/components/esp32_improv/* @jesserockz
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
esphome/components/ezo/* @ssieb
 | 
			
		||||
 
 | 
			
		||||
@@ -394,7 +394,7 @@ def command_update_all(args):
 | 
			
		||||
    import click
 | 
			
		||||
 | 
			
		||||
    success = {}
 | 
			
		||||
    files = list_yaml_files(args.configuration)
 | 
			
		||||
    files = list_yaml_files(args.configuration[0])
 | 
			
		||||
    twidth = 60
 | 
			
		||||
 | 
			
		||||
    def print_bar(middle_text):
 | 
			
		||||
@@ -408,7 +408,7 @@ def command_update_all(args):
 | 
			
		||||
        print("-" * twidth)
 | 
			
		||||
        print()
 | 
			
		||||
        rc = run_external_process(
 | 
			
		||||
            "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
 | 
			
		||||
            "esphome", "--dashboard", "run", "--no-logs", "--device", "OTA", f
 | 
			
		||||
        )
 | 
			
		||||
        if rc == 0:
 | 
			
		||||
            print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f))
 | 
			
		||||
@@ -504,6 +504,7 @@ def parse_args(argv):
 | 
			
		||||
            "version",
 | 
			
		||||
            "clean",
 | 
			
		||||
            "dashboard",
 | 
			
		||||
            "vscode",
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@@ -686,7 +687,7 @@ def run_esphome(argv):
 | 
			
		||||
    CORE.dashboard = args.dashboard
 | 
			
		||||
 | 
			
		||||
    setup_log(args.verbose, args.quiet)
 | 
			
		||||
    if args.deprecated_argv_suggestion is not None:
 | 
			
		||||
    if args.deprecated_argv_suggestion is not None and args.command != "vscode":
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "Calling ESPHome with the configuration before the command is deprecated "
 | 
			
		||||
            "and will be removed in the future. "
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ void BangBangClimate::setup() {
 | 
			
		||||
    restore->to_call(this).perform();
 | 
			
		||||
  } else {
 | 
			
		||||
    // restore from defaults, change_away handles those for us
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    this->change_away_(false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -41,7 +41,7 @@ void BangBangClimate::control(const climate::ClimateCall &call) {
 | 
			
		||||
climate::ClimateTraits BangBangClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_supports_auto_mode(true);
 | 
			
		||||
  traits.set_supports_heat_cool_mode(true);
 | 
			
		||||
  traits.set_supports_cool_mode(this->supports_cool_);
 | 
			
		||||
  traits.set_supports_heat_mode(this->supports_heat_);
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(true);
 | 
			
		||||
@@ -50,7 +50,7 @@ climate::ClimateTraits BangBangClimate::traits() {
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
void BangBangClimate::compute_state_() {
 | 
			
		||||
  if (this->mode != climate::CLIMATE_MODE_AUTO) {
 | 
			
		||||
  if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) {
 | 
			
		||||
    // in non-auto mode, switch directly to appropriate action
 | 
			
		||||
    //  - HEAT mode -> HEATING action
 | 
			
		||||
    //  - COOL mode -> COOLING action
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ ClimateTraits = climate_ns.class_("ClimateTraits")
 | 
			
		||||
ClimateMode = climate_ns.enum("ClimateMode")
 | 
			
		||||
CLIMATE_MODES = {
 | 
			
		||||
    "OFF": ClimateMode.CLIMATE_MODE_OFF,
 | 
			
		||||
    "HEAT_COOL": ClimateMode.CLIMATE_HEAT_COOL,
 | 
			
		||||
    "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL,
 | 
			
		||||
    "COOL": ClimateMode.CLIMATE_MODE_COOL,
 | 
			
		||||
    "HEAT": ClimateMode.CLIMATE_MODE_HEAT,
 | 
			
		||||
    "DRY": ClimateMode.CLIMATE_MODE_DRY,
 | 
			
		||||
 
 | 
			
		||||
@@ -159,7 +159,7 @@ struct ClimateDeviceRestoreState {
 | 
			
		||||
 *
 | 
			
		||||
 * The entire state of the climate device is encoded in public properties of the base class (current_temperature,
 | 
			
		||||
 * mode etc). These are read-only for the user and rw for integrations. The reason these are public
 | 
			
		||||
 * is for simple access to them from lambdas `if (id(my_climate).mode == climate::CLIMATE_MODE_AUTO) ...`
 | 
			
		||||
 * is for simple access to them from lambdas `if (id(my_climate).mode == climate::CLIMATE_MODE_HEAT_COOL) ...`
 | 
			
		||||
 */
 | 
			
		||||
class Climate : public Nameable {
 | 
			
		||||
 public:
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ bool ClimateTraits::supports_mode(ClimateMode mode) const {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case CLIMATE_MODE_OFF:
 | 
			
		||||
      return true;
 | 
			
		||||
    case CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      return this->supports_heat_cool_mode_;
 | 
			
		||||
    case CLIMATE_MODE_AUTO:
 | 
			
		||||
      return this->supports_auto_mode_;
 | 
			
		||||
    case CLIMATE_MODE_COOL:
 | 
			
		||||
@@ -31,6 +33,9 @@ void ClimateTraits::set_supports_two_point_target_temperature(bool supports_two_
 | 
			
		||||
  supports_two_point_target_temperature_ = supports_two_point_target_temperature;
 | 
			
		||||
}
 | 
			
		||||
void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_auto_mode_ = supports_auto_mode; }
 | 
			
		||||
void ClimateTraits::set_supports_heat_cool_mode(bool supports_heat_cool_mode) {
 | 
			
		||||
  supports_heat_cool_mode_ = supports_heat_cool_mode;
 | 
			
		||||
}
 | 
			
		||||
void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; }
 | 
			
		||||
void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; }
 | 
			
		||||
void ClimateTraits::set_supports_fan_only_mode(bool supports_fan_only_mode) {
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,7 @@ class ClimateTraits {
 | 
			
		||||
  bool get_supports_two_point_target_temperature() const;
 | 
			
		||||
  void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature);
 | 
			
		||||
  void set_supports_auto_mode(bool supports_auto_mode);
 | 
			
		||||
  void set_supports_heat_cool_mode(bool supports_heat_cool_mode);
 | 
			
		||||
  void set_supports_cool_mode(bool supports_cool_mode);
 | 
			
		||||
  void set_supports_heat_mode(bool supports_heat_mode);
 | 
			
		||||
  void set_supports_fan_only_mode(bool supports_fan_only_mode);
 | 
			
		||||
@@ -100,6 +101,7 @@ class ClimateTraits {
 | 
			
		||||
  bool supports_current_temperature_{false};
 | 
			
		||||
  bool supports_two_point_target_temperature_{false};
 | 
			
		||||
  bool supports_auto_mode_{false};
 | 
			
		||||
  bool supports_heat_cool_mode_{false};
 | 
			
		||||
  bool supports_cool_mode_{false};
 | 
			
		||||
  bool supports_heat_mode_{false};
 | 
			
		||||
  bool supports_fan_only_mode_{false};
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ static const char *TAG = "climate_ir";
 | 
			
		||||
climate::ClimateTraits ClimateIR::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.set_supports_current_temperature(this->sensor_ != nullptr);
 | 
			
		||||
  traits.set_supports_auto_mode(true);
 | 
			
		||||
  traits.set_supports_heat_cool_mode(true);
 | 
			
		||||
  traits.set_supports_cool_mode(this->supports_cool_);
 | 
			
		||||
  traits.set_supports_heat_mode(this->supports_heat_);
 | 
			
		||||
  traits.set_supports_dry_mode(this->supports_dry_);
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ void LgIrClimate::transmit_state() {
 | 
			
		||||
    send_swing_cmd_ = false;
 | 
			
		||||
    remote_state |= COMMAND_SWING;
 | 
			
		||||
  } else {
 | 
			
		||||
    if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_AUTO) {
 | 
			
		||||
    if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
 | 
			
		||||
      remote_state |= COMMAND_ON_AI;
 | 
			
		||||
    } else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) {
 | 
			
		||||
      remote_state |= COMMAND_ON;
 | 
			
		||||
@@ -52,7 +52,7 @@ void LgIrClimate::transmit_state() {
 | 
			
		||||
        case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
          remote_state |= COMMAND_HEAT;
 | 
			
		||||
          break;
 | 
			
		||||
        case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
        case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
          remote_state |= COMMAND_AUTO;
 | 
			
		||||
          break;
 | 
			
		||||
        case climate::CLIMATE_MODE_DRY:
 | 
			
		||||
@@ -89,7 +89,7 @@ void LgIrClimate::transmit_state() {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->mode == climate::CLIMATE_MODE_AUTO) {
 | 
			
		||||
    if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_AUTO;
 | 
			
		||||
      // remote_state |= FAN_MODE_AUTO_DRY;
 | 
			
		||||
    }
 | 
			
		||||
@@ -128,7 +128,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
  if ((remote_state & COMMAND_MASK) == COMMAND_ON) {
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_COOL;
 | 
			
		||||
  } else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) {
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((remote_state & COMMAND_MASK) == COMMAND_OFF) {
 | 
			
		||||
@@ -138,7 +138,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
        this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
 | 
			
		||||
  } else {
 | 
			
		||||
    if ((remote_state & COMMAND_MASK) == COMMAND_AUTO)
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN)
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_DRY;
 | 
			
		||||
    else if ((remote_state & COMMAND_MASK) == COMMAND_HEAT) {
 | 
			
		||||
@@ -152,7 +152,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
      this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15;
 | 
			
		||||
 | 
			
		||||
    // Fan Speed
 | 
			
		||||
    if (this->mode == climate::CLIMATE_MODE_AUTO) {
 | 
			
		||||
    if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_AUTO;
 | 
			
		||||
    } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT ||
 | 
			
		||||
               this->mode == climate::CLIMATE_MODE_DRY) {
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,7 @@ void CoolixClimate::transmit_state() {
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
        remote_state |= COOLIX_HEAT;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
        remote_state |= COOLIX_AUTO;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
@@ -89,7 +89,7 @@ void CoolixClimate::transmit_state() {
 | 
			
		||||
      } else {
 | 
			
		||||
        remote_state |= COOLIX_FAN_TEMP_CODE;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->mode == climate::CLIMATE_MODE_AUTO || this->mode == climate::CLIMATE_MODE_DRY) {
 | 
			
		||||
      if (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_DRY) {
 | 
			
		||||
        this->fan_mode = climate::CLIMATE_FAN_AUTO;
 | 
			
		||||
        remote_state |= COOLIX_FAN_MODE_AUTO_DRY;
 | 
			
		||||
      } else {
 | 
			
		||||
@@ -197,7 +197,7 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
    if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT)
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
    else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO)
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) {
 | 
			
		||||
      if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_MODE_AUTO_DRY)
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_DRY;
 | 
			
		||||
@@ -207,7 +207,7 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_COOL;
 | 
			
		||||
 | 
			
		||||
    // Fan Speed
 | 
			
		||||
    if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_AUTO ||
 | 
			
		||||
    if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_HEAT_COOL ||
 | 
			
		||||
        this->mode == climate::CLIMATE_MODE_DRY)
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_AUTO;
 | 
			
		||||
    else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN)
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,9 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
 | 
			
		||||
    "cse7766", baud_rate=4800, require_rx=True
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
@@ -64,9 +67,3 @@ async def to_code(config):
 | 
			
		||||
        conf = config[CONF_POWER]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_power_sensor(sens))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate(config, item_config):
 | 
			
		||||
    uart.validate_device(
 | 
			
		||||
        "cse7766", config, item_config, baud_rate=4800, require_tx=False
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@ uint8_t DaikinClimate::operation_mode_() {
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
      operating_mode |= DAIKIN_MODE_HEAT;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      operating_mode |= DAIKIN_MODE_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
@@ -131,7 +131,7 @@ uint8_t DaikinClimate::temperature_() {
 | 
			
		||||
  switch (this->mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
      return 0x32;
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
    case climate::CLIMATE_MODE_DRY:
 | 
			
		||||
      return 0xc0;
 | 
			
		||||
    default:
 | 
			
		||||
@@ -160,7 +160,7 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
        break;
 | 
			
		||||
      case DAIKIN_MODE_AUTO:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
        break;
 | 
			
		||||
      case DAIKIN_MODE_FAN:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
 
 | 
			
		||||
@@ -68,6 +68,9 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
 | 
			
		||||
    "dfplayer", baud_rate=9600, require_tx=True
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
@@ -80,12 +83,6 @@ async def to_code(config):
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate(config, item_config):
 | 
			
		||||
    uart.validate_device(
 | 
			
		||||
        "dfplayer", config, item_config, baud_rate=9600, require_rx=False
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "dfplayer.play_next",
 | 
			
		||||
    NextAction,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,18 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_MODEL, ESP_PLATFORM_ESP32
 | 
			
		||||
from esphome.const import CONF_ID, ESP_PLATFORM_ESP32
 | 
			
		||||
 | 
			
		||||
ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
 | 
			
		||||
CONF_MANUFACTURER = "manufacturer"
 | 
			
		||||
CONF_SERVER = "server"
 | 
			
		||||
CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"]
 | 
			
		||||
 | 
			
		||||
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
 | 
			
		||||
ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component)
 | 
			
		||||
BLEServer = esp32_ble_ns.class_("BLEServer", cg.Component)
 | 
			
		||||
 | 
			
		||||
BLEServiceComponent = esp32_ble_ns.class_("BLEServiceComponent")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(ESP32BLE),
 | 
			
		||||
        cv.Optional(CONF_SERVER): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(): cv.declare_id(BLEServer),
 | 
			
		||||
                cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string,
 | 
			
		||||
                cv.Optional(CONF_MODEL): cv.string,
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
@@ -32,13 +20,3 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_SERVER in config:
 | 
			
		||||
        conf = config[CONF_SERVER]
 | 
			
		||||
        server = cg.new_Pvariable(conf[CONF_ID])
 | 
			
		||||
        await cg.register_component(server, conf)
 | 
			
		||||
        cg.add(server.set_manufacturer(conf[CONF_MANUFACTURER]))
 | 
			
		||||
        if CONF_MODEL in conf:
 | 
			
		||||
            cg.add(server.set_model(conf[CONF_MODEL]))
 | 
			
		||||
        cg.add_define("USE_ESP32_BLE_SERVER")
 | 
			
		||||
        cg.add(var.set_server(server))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#include "ble.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
@@ -26,14 +27,22 @@ void ESP32BLE::setup() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->advertising_ = new BLEAdvertising();
 | 
			
		||||
 | 
			
		||||
  this->advertising_->set_scan_response(true);
 | 
			
		||||
  this->advertising_->set_min_preferred_interval(0x06);
 | 
			
		||||
  this->advertising_->start();
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "BLE setup complete");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::mark_failed() {
 | 
			
		||||
  Component::mark_failed();
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
  if (this->server_ != nullptr) {
 | 
			
		||||
    this->server_->mark_failed();
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESP32BLE::ble_setup_() {
 | 
			
		||||
@@ -82,7 +91,16 @@ bool ESP32BLE::ble_setup_() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = esp_ble_gap_set_device_name(App.get_name().c_str());
 | 
			
		||||
  std::string name = App.get_name();
 | 
			
		||||
  if (name.length() > 20) {
 | 
			
		||||
    if (App.is_name_add_mac_suffix_enabled()) {
 | 
			
		||||
      name.erase(name.begin() + 13, name.end() - 7);  // Remove characters between 13 and the mac address
 | 
			
		||||
    } else {
 | 
			
		||||
      name = name.substr(0, 20);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = esp_ble_gap_set_device_name(name.c_str());
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err);
 | 
			
		||||
    return false;
 | 
			
		||||
@@ -142,7 +160,9 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
 | 
			
		||||
void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
                                         esp_ble_gatts_cb_param_t *param) {
 | 
			
		||||
  ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
  this->server_->gatts_event_handler(event, gatts_if, param);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,21 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "ble_advertising.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "ble_server.h"
 | 
			
		||||
#include "queue.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_server.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gap_ble_api.h>
 | 
			
		||||
#include <esp_gatts_api.h>
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
 | 
			
		||||
@@ -28,11 +33,20 @@ class ESP32BLE : public Component {
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void mark_failed() override;
 | 
			
		||||
 | 
			
		||||
  bool has_server() { return this->server_ != nullptr; }
 | 
			
		||||
  bool has_server() {
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
    return this->server_ != nullptr;
 | 
			
		||||
#else
 | 
			
		||||
    return false;
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
  bool has_client() { return false; }
 | 
			
		||||
 | 
			
		||||
  void set_server(BLEServer *server) { this->server_ = server; }
 | 
			
		||||
  BLEAdvertising *get_advertising() { return this->advertising_; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
  void set_server(esp32_ble_server::BLEServer *server) { this->server_ = server; }
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
 | 
			
		||||
  static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
 | 
			
		||||
@@ -44,8 +58,11 @@ class ESP32BLE : public Component {
 | 
			
		||||
 | 
			
		||||
  bool ble_setup_();
 | 
			
		||||
 | 
			
		||||
  BLEServer *server_{nullptr};
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
  esp32_ble_server::BLEServer *server_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  Queue<BLEEvent> ble_events_;
 | 
			
		||||
  BLEAdvertising *advertising_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern ESP32BLE *global_ble;
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,10 @@ BLEAdvertising::BLEAdvertising() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEAdvertising::add_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.push_back(uuid); }
 | 
			
		||||
void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) {
 | 
			
		||||
  this->advertising_uuids_.erase(std::remove(this->advertising_uuids_.begin(), this->advertising_uuids_.end(), uuid),
 | 
			
		||||
                                 this->advertising_uuids_.end());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEAdvertising::start() {
 | 
			
		||||
  int num_services = this->advertising_uuids_.size();
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ class BLEAdvertising {
 | 
			
		||||
  BLEAdvertising();
 | 
			
		||||
 | 
			
		||||
  void add_service_uuid(ESPBTUUID uuid);
 | 
			
		||||
  void remove_service_uuid(ESPBTUUID uuid);
 | 
			
		||||
  void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
 | 
			
		||||
  void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								esphome/components/esp32_ble_server/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								esphome/components/esp32_ble_server/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_MODEL, ESP_PLATFORM_ESP32
 | 
			
		||||
from esphome.components import esp32_ble
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"]
 | 
			
		||||
ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
 | 
			
		||||
 | 
			
		||||
CONF_MANUFACTURER = "manufacturer"
 | 
			
		||||
CONF_BLE_ID = "ble_id"
 | 
			
		||||
 | 
			
		||||
esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server")
 | 
			
		||||
BLEServer = esp32_ble_server_ns.class_("BLEServer", cg.Component)
 | 
			
		||||
BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(BLEServer),
 | 
			
		||||
        cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
 | 
			
		||||
        cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string,
 | 
			
		||||
        cv.Optional(CONF_MODEL): cv.string,
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_BLE_ID])
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_manufacturer(config[CONF_MANUFACTURER]))
 | 
			
		||||
    if CONF_MODEL in config:
 | 
			
		||||
        cg.add(var.set_model(config[CONF_MODEL]))
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_SERVER")
 | 
			
		||||
 | 
			
		||||
    cg.add(parent.set_server(var))
 | 
			
		||||
@@ -1,18 +1,18 @@
 | 
			
		||||
#include "ble_2901.h"
 | 
			
		||||
#include "ble_uuid.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
BLE2901::BLE2901(const std::string &value) : BLE2901((uint8_t *) value.data(), value.length()) {}
 | 
			
		||||
BLE2901::BLE2901(const uint8_t *data, size_t length) : BLEDescriptor(ESPBTUUID::from_uint16(0x2901)) {
 | 
			
		||||
BLE2901::BLE2901(const uint8_t *data, size_t length) : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2901)) {
 | 
			
		||||
  this->set_value(data, length);
 | 
			
		||||
  this->permissions_ = ESP_GATT_PERM_READ;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
class BLE2901 : public BLEDescriptor {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -13,7 +13,7 @@ class BLE2901 : public BLEDescriptor {
 | 
			
		||||
  BLE2901(const uint8_t *data, size_t length);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,18 +1,18 @@
 | 
			
		||||
#include "ble_2902.h"
 | 
			
		||||
#include "ble_uuid.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
BLE2902::BLE2902() : BLEDescriptor(ESPBTUUID::from_uint16(0x2902)) {
 | 
			
		||||
BLE2902::BLE2902() : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2902)) {
 | 
			
		||||
  this->value_.attr_len = 2;
 | 
			
		||||
  uint8_t data[2] = {0, 0};
 | 
			
		||||
  memcpy(this->value_.attr_value, data, 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -5,14 +5,14 @@
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
class BLE2902 : public BLEDescriptor {
 | 
			
		||||
 public:
 | 
			
		||||
  BLE2902();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -7,9 +7,9 @@
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "esp32_ble.characteristic";
 | 
			
		||||
static const char *const TAG = "esp32_ble_server.characteristic";
 | 
			
		||||
 | 
			
		||||
BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) {
 | 
			
		||||
  this->set_value_lock_ = xSemaphoreCreateBinary();
 | 
			
		||||
@@ -300,7 +300,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include "ble_uuid.h"
 | 
			
		||||
#include "ble_descriptor.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -14,7 +14,9 @@
 | 
			
		||||
#include <esp_bt_defs.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble;
 | 
			
		||||
 | 
			
		||||
class BLEService;
 | 
			
		||||
 | 
			
		||||
@@ -89,7 +91,7 @@ class BLECharacteristic {
 | 
			
		||||
  } state_{INIT};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -7,9 +7,9 @@
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "esp32_ble.descriptor";
 | 
			
		||||
static const char *const TAG = "esp32_ble_server.descriptor";
 | 
			
		||||
 | 
			
		||||
BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len) {
 | 
			
		||||
  this->uuid_ = uuid;
 | 
			
		||||
@@ -71,7 +71,7 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "ble_uuid.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -8,7 +8,9 @@
 | 
			
		||||
#include <esp_gatts_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble;
 | 
			
		||||
 | 
			
		||||
class BLECharacteristic;
 | 
			
		||||
 | 
			
		||||
@@ -43,7 +45,7 @@ class BLEDescriptor {
 | 
			
		||||
  } state_{INIT};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "ble_server.h"
 | 
			
		||||
#include "ble.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble/ble.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
@@ -14,11 +15,11 @@
 | 
			
		||||
#include <esp_gap_ble_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "esp32_ble.server";
 | 
			
		||||
static const char *const TAG = "esp32_ble_server";
 | 
			
		||||
 | 
			
		||||
static const uint16_t device_information_service__UUID = 0x180A;
 | 
			
		||||
static const uint16_t DEVICE_INFORMATION_SERVICE_UUID = 0x180A;
 | 
			
		||||
static const uint16_t MODEL_UUID = 0x2A24;
 | 
			
		||||
static const uint16_t VERSION_UUID = 0x2A26;
 | 
			
		||||
static const uint16_t MANUFACTURER_UUID = 0x2A29;
 | 
			
		||||
@@ -32,8 +33,6 @@ void BLEServer::setup() {
 | 
			
		||||
  ESP_LOGD(TAG, "Setting up BLE Server...");
 | 
			
		||||
 | 
			
		||||
  global_ble_server = this;
 | 
			
		||||
 | 
			
		||||
  this->advertising_ = new BLEAdvertising();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEServer::loop() {
 | 
			
		||||
@@ -53,35 +52,27 @@ void BLEServer::loop() {
 | 
			
		||||
    }
 | 
			
		||||
    case REGISTERING: {
 | 
			
		||||
      if (this->registered_) {
 | 
			
		||||
        this->device_information_service_ = this->create_service(device_information_service__UUID);
 | 
			
		||||
        this->device_information_service_ = this->create_service(DEVICE_INFORMATION_SERVICE_UUID);
 | 
			
		||||
 | 
			
		||||
        this->create_device_characteristics_();
 | 
			
		||||
 | 
			
		||||
        this->advertising_->set_scan_response(true);
 | 
			
		||||
        this->advertising_->set_min_preferred_interval(0x06);
 | 
			
		||||
        this->advertising_->start();
 | 
			
		||||
 | 
			
		||||
        this->state_ = STARTING_SERVICE;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case STARTING_SERVICE: {
 | 
			
		||||
      if (!this->device_information_service_->is_created()) {
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->device_information_service_->is_running()) {
 | 
			
		||||
        for (auto *component : this->service_components_) {
 | 
			
		||||
          component->setup_service();
 | 
			
		||||
        }
 | 
			
		||||
        this->state_ = SETTING_UP_COMPONENT_SERVICES;
 | 
			
		||||
        this->state_ = RUNNING;
 | 
			
		||||
        this->can_proceed_ = true;
 | 
			
		||||
        ESP_LOGD(TAG, "BLE server setup successfully");
 | 
			
		||||
      } else if (!this->device_information_service_->is_starting()) {
 | 
			
		||||
        this->device_information_service_->start();
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case SETTING_UP_COMPONENT_SERVICES: {
 | 
			
		||||
      this->state_ = RUNNING;
 | 
			
		||||
      this->can_proceed_ = true;
 | 
			
		||||
      ESP_LOGD(TAG, "BLE server setup successfully");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -123,7 +114,7 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
 | 
			
		||||
  BLEService *service = new BLEService(uuid, num_handles, inst_id);
 | 
			
		||||
  this->services_.push_back(service);
 | 
			
		||||
  if (advertise) {
 | 
			
		||||
    this->advertising_->add_service_uuid(uuid);
 | 
			
		||||
    esp32_ble::global_ble->get_advertising()->add_service_uuid(uuid);
 | 
			
		||||
  }
 | 
			
		||||
  service->do_create(this);
 | 
			
		||||
  return service;
 | 
			
		||||
@@ -145,7 +136,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
 | 
			
		||||
      ESP_LOGD(TAG, "BLE Client disconnected");
 | 
			
		||||
      if (this->remove_client_(param->disconnect.conn_id))
 | 
			
		||||
        this->connected_clients_--;
 | 
			
		||||
      this->advertising_->start();
 | 
			
		||||
      esp32_ble::global_ble->get_advertising()->start();
 | 
			
		||||
      for (auto *component : this->service_components_) {
 | 
			
		||||
        component->on_client_disconnect();
 | 
			
		||||
      }
 | 
			
		||||
@@ -171,7 +162,7 @@ void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); }
 | 
			
		||||
 | 
			
		||||
BLEServer *global_ble_server = nullptr;
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,15 +1,16 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "ble_service.h"
 | 
			
		||||
#include "ble_characteristic.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_advertising.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/queue.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "ble_service.h"
 | 
			
		||||
#include "ble_characteristic.h"
 | 
			
		||||
#include "ble_uuid.h"
 | 
			
		||||
#include "ble_advertising.h"
 | 
			
		||||
#include <map>
 | 
			
		||||
 | 
			
		||||
#include "queue.h"
 | 
			
		||||
#include <map>
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -17,11 +18,12 @@
 | 
			
		||||
#include <esp_gatts_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble;
 | 
			
		||||
 | 
			
		||||
class BLEServiceComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual void setup_service();
 | 
			
		||||
  virtual void on_client_connect(){};
 | 
			
		||||
  virtual void on_client_disconnect(){};
 | 
			
		||||
  virtual void start();
 | 
			
		||||
@@ -49,7 +51,6 @@ class BLEServer : public Component {
 | 
			
		||||
  esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
 | 
			
		||||
  uint32_t get_connected_client_count() { return this->connected_clients_; }
 | 
			
		||||
  const std::map<uint16_t, void *> &get_clients() { return this->clients_; }
 | 
			
		||||
  BLEAdvertising *get_advertising() { return this->advertising_; }
 | 
			
		||||
 | 
			
		||||
  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
 | 
			
		||||
 | 
			
		||||
@@ -69,7 +70,6 @@ class BLEServer : public Component {
 | 
			
		||||
  optional<std::string> model_;
 | 
			
		||||
  esp_gatt_if_t gatts_if_{0};
 | 
			
		||||
  bool registered_{false};
 | 
			
		||||
  BLEAdvertising *advertising_;
 | 
			
		||||
 | 
			
		||||
  uint32_t connected_clients_{0};
 | 
			
		||||
  std::map<uint16_t, void *> clients_;
 | 
			
		||||
@@ -83,14 +83,13 @@ class BLEServer : public Component {
 | 
			
		||||
    INIT = 0x00,
 | 
			
		||||
    REGISTERING,
 | 
			
		||||
    STARTING_SERVICE,
 | 
			
		||||
    SETTING_UP_COMPONENT_SERVICES,
 | 
			
		||||
    RUNNING,
 | 
			
		||||
  } state_{INIT};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern BLEServer *global_ble_server;
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -5,9 +5,9 @@
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "esp32_ble.service";
 | 
			
		||||
static const char *const TAG = "esp32_ble_server.service";
 | 
			
		||||
 | 
			
		||||
BLEService::BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id)
 | 
			
		||||
    : uuid_(uuid), num_handles_(num_handles), inst_id_(inst_id) {}
 | 
			
		||||
@@ -136,7 +136,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "ble_uuid.h"
 | 
			
		||||
#include "ble_characteristic.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -12,10 +12,12 @@
 | 
			
		||||
#include <esp_bt_defs.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
class BLEServer;
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble;
 | 
			
		||||
 | 
			
		||||
class BLEService {
 | 
			
		||||
 public:
 | 
			
		||||
  BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id);
 | 
			
		||||
@@ -73,7 +75,7 @@ class BLEService {
 | 
			
		||||
  } running_state_{STOPPED};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esp32_ble_server
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import binary_sensor, output, esp32_ble
 | 
			
		||||
from esphome.components import binary_sensor, output, esp32_ble_server
 | 
			
		||||
from esphome.const import CONF_ID, ESP_PLATFORM_ESP32
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["binary_sensor", "output", "improv"]
 | 
			
		||||
AUTO_LOAD = ["binary_sensor", "output", "improv", "esp32_ble_server"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["esp32_ble", "wifi"]
 | 
			
		||||
CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"]
 | 
			
		||||
DEPENDENCIES = ["wifi"]
 | 
			
		||||
ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
 | 
			
		||||
 | 
			
		||||
CONF_AUTHORIZED_DURATION = "authorized_duration"
 | 
			
		||||
@@ -18,7 +19,7 @@ CONF_WIFI_TIMEOUT = "wifi_timeout"
 | 
			
		||||
 | 
			
		||||
esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
 | 
			
		||||
ESP32ImprovComponent = esp32_improv_ns.class_(
 | 
			
		||||
    "ESP32ImprovComponent", cg.Component, esp32_ble.BLEServiceComponent
 | 
			
		||||
    "ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +34,7 @@ def validate_none_(value):
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
 | 
			
		||||
        cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble.BLEServer),
 | 
			
		||||
        cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer),
 | 
			
		||||
        cv.Required(CONF_AUTHORIZER): cv.Any(
 | 
			
		||||
            validate_none_, cv.use_id(binary_sensor.BinarySensor)
 | 
			
		||||
        ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
#include "esp32_improv_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble/ble.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_2902.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -12,40 +14,39 @@ static const char *TAG = "esp32_improv.component";
 | 
			
		||||
 | 
			
		||||
ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; }
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::setup_service() {
 | 
			
		||||
  this->service_ = esp32_ble::global_ble_server->create_service(improv::SERVICE_UUID, true);
 | 
			
		||||
void ESP32ImprovComponent::setup() {
 | 
			
		||||
  this->service_ = global_ble_server->create_service(improv::SERVICE_UUID, true);
 | 
			
		||||
  this->setup_characteristics();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::setup_characteristics() {
 | 
			
		||||
  this->status_ = this->service_->create_characteristic(
 | 
			
		||||
      improv::STATUS_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ | esp32_ble::BLECharacteristic::PROPERTY_NOTIFY);
 | 
			
		||||
  esp32_ble::BLEDescriptor *status_descriptor = new esp32_ble::BLE2902();
 | 
			
		||||
      improv::STATUS_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
 | 
			
		||||
  BLEDescriptor *status_descriptor = new BLE2902();
 | 
			
		||||
  this->status_->add_descriptor(status_descriptor);
 | 
			
		||||
 | 
			
		||||
  this->error_ = this->service_->create_characteristic(
 | 
			
		||||
      improv::ERROR_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ | esp32_ble::BLECharacteristic::PROPERTY_NOTIFY);
 | 
			
		||||
  esp32_ble::BLEDescriptor *error_descriptor = new esp32_ble::BLE2902();
 | 
			
		||||
      improv::ERROR_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
 | 
			
		||||
  BLEDescriptor *error_descriptor = new BLE2902();
 | 
			
		||||
  this->error_->add_descriptor(error_descriptor);
 | 
			
		||||
 | 
			
		||||
  this->rpc_ =
 | 
			
		||||
      this->service_->create_characteristic(improv::RPC_COMMAND_UUID, esp32_ble::BLECharacteristic::PROPERTY_WRITE);
 | 
			
		||||
  this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
 | 
			
		||||
  this->rpc_->on_write([this](const std::vector<uint8_t> &data) {
 | 
			
		||||
    if (data.size() > 0) {
 | 
			
		||||
      this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  esp32_ble::BLEDescriptor *rpc_descriptor = new esp32_ble::BLE2902();
 | 
			
		||||
  BLEDescriptor *rpc_descriptor = new BLE2902();
 | 
			
		||||
  this->rpc_->add_descriptor(rpc_descriptor);
 | 
			
		||||
 | 
			
		||||
  this->rpc_response_ =
 | 
			
		||||
      this->service_->create_characteristic(improv::RPC_RESULT_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ |
 | 
			
		||||
                                                                         esp32_ble::BLECharacteristic::PROPERTY_NOTIFY);
 | 
			
		||||
  esp32_ble::BLEDescriptor *rpc_response_descriptor = new esp32_ble::BLE2902();
 | 
			
		||||
  this->rpc_response_ = this->service_->create_characteristic(
 | 
			
		||||
      improv::RPC_RESULT_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
 | 
			
		||||
  BLEDescriptor *rpc_response_descriptor = new BLE2902();
 | 
			
		||||
  this->rpc_response_->add_descriptor(rpc_response_descriptor);
 | 
			
		||||
 | 
			
		||||
  this->capabilities_ =
 | 
			
		||||
      this->service_->create_characteristic(improv::CAPABILITIES_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ);
 | 
			
		||||
  esp32_ble::BLEDescriptor *capabilities_descriptor = new esp32_ble::BLE2902();
 | 
			
		||||
      this->service_->create_characteristic(improv::CAPABILITIES_UUID, BLECharacteristic::PROPERTY_READ);
 | 
			
		||||
  BLEDescriptor *capabilities_descriptor = new BLE2902();
 | 
			
		||||
  this->capabilities_->add_descriptor(capabilities_descriptor);
 | 
			
		||||
  uint8_t capabilities = 0x00;
 | 
			
		||||
  if (this->status_indicator_ != nullptr)
 | 
			
		||||
@@ -64,13 +65,9 @@ void ESP32ImprovComponent::loop() {
 | 
			
		||||
      if (this->status_indicator_ != nullptr)
 | 
			
		||||
        this->status_indicator_->turn_off();
 | 
			
		||||
 | 
			
		||||
      if (this->service_->is_created() && !this->setup_complete_) {
 | 
			
		||||
        this->setup_characteristics();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->should_start_ && this->setup_complete_) {
 | 
			
		||||
      if (this->service_->is_created() && this->should_start_ && this->setup_complete_) {
 | 
			
		||||
        if (this->service_->is_running()) {
 | 
			
		||||
          this->service_->get_server()->get_advertising()->start();
 | 
			
		||||
          esp32_ble::global_ble->get_advertising()->start();
 | 
			
		||||
 | 
			
		||||
          this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
 | 
			
		||||
          this->set_error_(improv::ERROR_NONE);
 | 
			
		||||
@@ -205,10 +202,7 @@ void ESP32ImprovComponent::stop() {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ESP32ImprovComponent::get_setup_priority() const {
 | 
			
		||||
  // Before WiFi
 | 
			
		||||
  return setup_priority::AFTER_BLUETOOTH;
 | 
			
		||||
}
 | 
			
		||||
float ESP32ImprovComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ESP32 Improv:");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +1,28 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_server.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
 | 
			
		||||
#include "esphome/components/improv/improv.h"
 | 
			
		||||
#include "esphome/components/output/binary_output.h"
 | 
			
		||||
#include "esphome/components/wifi/wifi_component.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_server.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_characteristic.h"
 | 
			
		||||
#include "esphome/components/output/binary_output.h"
 | 
			
		||||
#include "esphome/components/wifi/wifi_component.h"
 | 
			
		||||
#include "esphome/components/improv/improv.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_improv {
 | 
			
		||||
 | 
			
		||||
class ESP32ImprovComponent : public Component, public esp32_ble::BLEServiceComponent {
 | 
			
		||||
using namespace esp32_ble_server;
 | 
			
		||||
 | 
			
		||||
class ESP32ImprovComponent : public Component, public BLEServiceComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  ESP32ImprovComponent();
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void setup_service() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void setup_characteristics();
 | 
			
		||||
  void on_client_disconnect() override;
 | 
			
		||||
 | 
			
		||||
@@ -46,12 +48,12 @@ class ESP32ImprovComponent : public Component, public esp32_ble::BLEServiceCompo
 | 
			
		||||
  std::vector<uint8_t> incoming_data_;
 | 
			
		||||
  wifi::WiFiAP connecting_sta_;
 | 
			
		||||
 | 
			
		||||
  esp32_ble::BLEService *service_;
 | 
			
		||||
  esp32_ble::BLECharacteristic *status_;
 | 
			
		||||
  esp32_ble::BLECharacteristic *error_;
 | 
			
		||||
  esp32_ble::BLECharacteristic *rpc_;
 | 
			
		||||
  esp32_ble::BLECharacteristic *rpc_response_;
 | 
			
		||||
  esp32_ble::BLECharacteristic *capabilities_;
 | 
			
		||||
  BLEService *service_;
 | 
			
		||||
  BLECharacteristic *status_;
 | 
			
		||||
  BLECharacteristic *error_;
 | 
			
		||||
  BLECharacteristic *rpc_;
 | 
			
		||||
  BLECharacteristic *rpc_response_;
 | 
			
		||||
  BLECharacteristic *capabilities_;
 | 
			
		||||
 | 
			
		||||
  binary_sensor::BinarySensor *authorizer_{nullptr};
 | 
			
		||||
  output::BinaryOutput *status_indicator_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -132,7 +132,7 @@ void FujitsuGeneralClimate::transmit_state() {
 | 
			
		||||
    case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
      SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_FAN);
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
    default:
 | 
			
		||||
      SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_AUTO);
 | 
			
		||||
      break;
 | 
			
		||||
@@ -343,7 +343,7 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
      case FUJITSU_GENERAL_MODE_AUTO:
 | 
			
		||||
      default:
 | 
			
		||||
        // TODO: CLIMATE_MODE_10C is missing from esphome
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    .extend(cv.polling_component_schema("20s"))
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
@@ -95,7 +96,3 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    # https://platformio.org/lib/show/1655/TinyGPSPlus
 | 
			
		||||
    cg.add_library("1655", "1.0.2")  # TinyGPSPlus, has name conflict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate(config, item_config):
 | 
			
		||||
    uart.validate_device("gps", config, item_config, require_tx=False)
 | 
			
		||||
 
 | 
			
		||||
@@ -155,7 +155,7 @@ void HitachiClimate::transmit_state() {
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
      set_mode_(HITACHI_AC344_MODE_HEAT);
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      set_mode_(HITACHI_AC344_MODE_AUTO);
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
@@ -251,7 +251,7 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
        break;
 | 
			
		||||
      case HITACHI_AC344_MODE_AUTO:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
        break;
 | 
			
		||||
      case HITACHI_AC344_MODE_FAN:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import urllib.parse as urlparse
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
@@ -14,7 +15,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_URL,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, Lambda
 | 
			
		||||
from esphome.core.config import PLATFORMIO_ESP8266_LUT
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
AUTO_LOAD = ["json"]
 | 
			
		||||
@@ -36,29 +36,6 @@ CONF_VERIFY_SSL = "verify_ssl"
 | 
			
		||||
CONF_ON_RESPONSE = "on_response"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_framework(config):
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        return config
 | 
			
		||||
 | 
			
		||||
    version = "RECOMMENDED"
 | 
			
		||||
    if CONF_ARDUINO_VERSION in CORE.raw_config[CONF_ESPHOME]:
 | 
			
		||||
        version = CORE.raw_config[CONF_ESPHOME][CONF_ARDUINO_VERSION]
 | 
			
		||||
 | 
			
		||||
    if version in ["LATEST", "DEV"]:
 | 
			
		||||
        return config
 | 
			
		||||
 | 
			
		||||
    framework = (
 | 
			
		||||
        PLATFORMIO_ESP8266_LUT[version]
 | 
			
		||||
        if version in PLATFORMIO_ESP8266_LUT
 | 
			
		||||
        else version
 | 
			
		||||
    )
 | 
			
		||||
    if framework < ARDUINO_VERSION_ESP8266["2.5.1"]:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "This component is not supported on arduino framework version below 2.5.1"
 | 
			
		||||
        )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_url(value):
 | 
			
		||||
    value = cv.string(value)
 | 
			
		||||
    try:
 | 
			
		||||
@@ -92,19 +69,36 @@ def validate_secure_url(config):
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(HttpRequestComponent),
 | 
			
		||||
            cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_TIMEOUT, default="5s"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .add_extra(validate_framework)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(HttpRequestComponent),
 | 
			
		||||
        cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string,
 | 
			
		||||
        cv.Optional(CONF_TIMEOUT, default="5s"): cv.positive_time_period_milliseconds,
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_framework(config):
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # only for ESP8266
 | 
			
		||||
    path = [CONF_ESPHOME, CONF_ARDUINO_VERSION]
 | 
			
		||||
    version: str = fv.full_config.get().get_config_for_path(path)
 | 
			
		||||
 | 
			
		||||
    reverse_map = {v: k for k, v in ARDUINO_VERSION_ESP8266.items()}
 | 
			
		||||
    framework_version = reverse_map.get(version)
 | 
			
		||||
    if framework_version is None or framework_version == "dev":
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if framework_version < "2.5.1":
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "This component is not supported on arduino framework version below 2.5.1",
 | 
			
		||||
            path=[cv.ROOT_CONFIG_PATH] + path,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.Schema(validate_framework)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
 
 | 
			
		||||
@@ -167,7 +167,7 @@ climate::ClimateTraits MideaAC::traits() {
 | 
			
		||||
  traits.set_visual_min_temperature(17);
 | 
			
		||||
  traits.set_visual_max_temperature(30);
 | 
			
		||||
  traits.set_visual_temperature_step(0.5);
 | 
			
		||||
  traits.set_supports_auto_mode(true);
 | 
			
		||||
  traits.set_supports_heat_cool_mode(true);
 | 
			
		||||
  traits.set_supports_cool_mode(true);
 | 
			
		||||
  traits.set_supports_dry_mode(true);
 | 
			
		||||
  traits.set_supports_heat_mode(true);
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,9 @@ const std::string MIDEA_SILENT_FAN_MODE = "silent";
 | 
			
		||||
const std::string MIDEA_TURBO_FAN_MODE = "turbo";
 | 
			
		||||
const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection";
 | 
			
		||||
 | 
			
		||||
const uint8_t QueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x00,
 | 
			
		||||
                                    0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
                                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x68};
 | 
			
		||||
const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81,
 | 
			
		||||
                                    0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
                                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31};
 | 
			
		||||
 | 
			
		||||
const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21,
 | 
			
		||||
                                         0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
@@ -44,7 +44,7 @@ climate::ClimateMode PropertiesFrame::get_mode() const {
 | 
			
		||||
    return climate::CLIMATE_MODE_OFF;
 | 
			
		||||
  switch (this->pbuf_[12] >> 5) {
 | 
			
		||||
    case MIDEA_MODE_AUTO:
 | 
			
		||||
      return climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
      return climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    case MIDEA_MODE_COOL:
 | 
			
		||||
      return climate::CLIMATE_MODE_COOL;
 | 
			
		||||
    case MIDEA_MODE_DRY:
 | 
			
		||||
@@ -61,7 +61,7 @@ climate::ClimateMode PropertiesFrame::get_mode() const {
 | 
			
		||||
void PropertiesFrame::set_mode(climate::ClimateMode mode) {
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      m = MIDEA_MODE_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_COOL:
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ void MitsubishiClimate::transmit_state() {
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
      remote_state[6] = MITSUBISHI_HEAT;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      remote_state[6] = MITSUBISHI_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_OFF:
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ void PIDClimate::setup() {
 | 
			
		||||
    restore->to_call(this).perform();
 | 
			
		||||
  } else {
 | 
			
		||||
    // restore from defaults, change_away handles those for us
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    this->target_temperature = this->default_target_temperature_;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,7 +31,7 @@ void PIDClimate::control(const climate::ClimateCall &call) {
 | 
			
		||||
    this->target_temperature = *call.get_target_temperature();
 | 
			
		||||
 | 
			
		||||
  // If switching to non-auto mode, set output immediately
 | 
			
		||||
  if (this->mode != climate::CLIMATE_MODE_AUTO)
 | 
			
		||||
  if (this->mode != climate::CLIMATE_MODE_HEAT_COOL)
 | 
			
		||||
    this->handle_non_auto_mode_();
 | 
			
		||||
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
@@ -39,7 +39,7 @@ void PIDClimate::control(const climate::ClimateCall &call) {
 | 
			
		||||
climate::ClimateTraits PIDClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_supports_auto_mode(true);
 | 
			
		||||
  traits.set_supports_heat_cool_mode(true);
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(false);
 | 
			
		||||
  traits.set_supports_cool_mode(this->supports_cool_());
 | 
			
		||||
  traits.set_supports_heat_mode(this->supports_heat_());
 | 
			
		||||
@@ -121,14 +121,14 @@ void PIDClimate::update_pid_() {
 | 
			
		||||
        // keep autotuner instance so that subsequent dump_configs will print the long result message.
 | 
			
		||||
      } else {
 | 
			
		||||
        value = res.output;
 | 
			
		||||
        if (mode != climate::CLIMATE_MODE_AUTO) {
 | 
			
		||||
        if (mode != climate::CLIMATE_MODE_HEAT_COOL) {
 | 
			
		||||
          ESP_LOGW(TAG, "For PID autotuner you need to set AUTO (also called heat/cool) mode!");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->mode != climate::CLIMATE_MODE_AUTO) {
 | 
			
		||||
  if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) {
 | 
			
		||||
    this->handle_non_auto_mode_();
 | 
			
		||||
  } else {
 | 
			
		||||
    this->write_output_(value);
 | 
			
		||||
 
 | 
			
		||||
@@ -19,13 +19,12 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    ).extend(spi.spi_device_schema(cs_pin_required=True))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
 | 
			
		||||
    "rc522_spi", require_miso=True, require_mosi=True
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await rc522.setup_rc522(var, config)
 | 
			
		||||
    await spi.register_spi_device(var, config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate(config, item_config):
 | 
			
		||||
    # validate given SPI hub is suitable for rc522_spi, it needs both miso and mosi
 | 
			
		||||
    spi.validate_device("rc522_spi", config, item_config, True, True)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import logging
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.components.output import FloatOutput
 | 
			
		||||
from esphome.const import CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_TRIGGER_ID
 | 
			
		||||
@@ -36,12 +37,8 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate(config, item_config):
 | 
			
		||||
    # Not adding this to FloatOutput as this is the only component which needs `update_frequency`
 | 
			
		||||
 | 
			
		||||
    parent_config = config.get_config_by_id(item_config[CONF_OUTPUT])
 | 
			
		||||
    platform = parent_config[CONF_PLATFORM]
 | 
			
		||||
 | 
			
		||||
def validate_parent_output_config(value):
 | 
			
		||||
    platform = value.get(CONF_PLATFORM)
 | 
			
		||||
    PWM_GOOD = ["esp8266_pwm", "ledc"]
 | 
			
		||||
    PWM_BAD = [
 | 
			
		||||
        "ac_dimmer ",
 | 
			
		||||
@@ -55,14 +52,25 @@ def validate(config, item_config):
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if platform in PWM_BAD:
 | 
			
		||||
        raise ValueError(f"Component rtttl cannot use {platform} as output component")
 | 
			
		||||
        raise cv.Invalid(f"Component rtttl cannot use {platform} as output component")
 | 
			
		||||
 | 
			
		||||
    if platform not in PWM_GOOD:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "Component rtttl is not known to work with the selected output type. Make sure this output supports custom frequency output method."
 | 
			
		||||
            "Component rtttl is not known to work with the selected output type. "
 | 
			
		||||
            "Make sure this output supports custom frequency output method."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_OUTPUT): fv.id_declaration_match_schema(
 | 
			
		||||
            validate_parent_output_config
 | 
			
		||||
        )
 | 
			
		||||
    },
 | 
			
		||||
    extra=cv.ALLOW_EXTRA,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,9 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    .extend(cv.polling_component_schema("5s"))
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
 | 
			
		||||
    "sim800l", baud_rate=9600, require_tx=True, require_rx=True
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
@@ -54,10 +57,6 @@ async def to_code(config):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate(config, item_config):
 | 
			
		||||
    uart.validate_device("sim800l", config, item_config, baud_rate=9600)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SIM800L_SEND_SMS_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(Sim800LComponent),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CLK_PIN,
 | 
			
		||||
@@ -69,9 +70,24 @@ async def register_spi_device(var, config):
 | 
			
		||||
        cg.add(var.set_cs_pin(pin))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_device(name, config, item_config, require_mosi, require_miso):
 | 
			
		||||
    spi_config = config.get_config_by_id(item_config[CONF_SPI_ID])
 | 
			
		||||
    if require_mosi and CONF_MISO_PIN not in spi_config:
 | 
			
		||||
        raise ValueError(f"Component {name} requires parent spi to declare miso_pin")
 | 
			
		||||
    if require_miso and CONF_MOSI_PIN not in spi_config:
 | 
			
		||||
        raise ValueError(f"Component {name} requires parent spi to declare mosi_pin")
 | 
			
		||||
def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: bool):
 | 
			
		||||
    hub_schema = {}
 | 
			
		||||
    if require_miso:
 | 
			
		||||
        hub_schema[
 | 
			
		||||
            cv.Required(
 | 
			
		||||
                CONF_MISO_PIN,
 | 
			
		||||
                msg=f"Component {name} requires this spi bus to declare a miso_pin",
 | 
			
		||||
            )
 | 
			
		||||
        ] = cv.valid
 | 
			
		||||
    if require_mosi:
 | 
			
		||||
        hub_schema[
 | 
			
		||||
            cv.Required(
 | 
			
		||||
                CONF_MOSI_PIN,
 | 
			
		||||
                msg=f"Component {name} requires this spi bus to declare a mosi_pin",
 | 
			
		||||
            )
 | 
			
		||||
        ] = cv.valid
 | 
			
		||||
 | 
			
		||||
    return cv.Schema(
 | 
			
		||||
        {cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)},
 | 
			
		||||
        extra=cv.ALLOW_EXTRA,
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ void Tcl112Climate::transmit_state() {
 | 
			
		||||
 | 
			
		||||
  // Set mode
 | 
			
		||||
  switch (this->mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      remote_state[6] &= 0xF0;
 | 
			
		||||
      remote_state[6] |= TCL112_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
@@ -204,7 +204,7 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
        break;
 | 
			
		||||
      case TCL112_AUTO:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -227,7 +227,7 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
 | 
			
		||||
    auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config
 | 
			
		||||
    heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config
 | 
			
		||||
    two_points_available = CONF_HEAT_ACTION in config and (
 | 
			
		||||
        CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config
 | 
			
		||||
    )
 | 
			
		||||
@@ -258,10 +258,10 @@ async def to_code(config):
 | 
			
		||||
        var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if auto_mode_available is True:
 | 
			
		||||
        cg.add(var.set_supports_auto(True))
 | 
			
		||||
    if heat_cool_mode_available is True:
 | 
			
		||||
        cg.add(var.set_supports_heat_cool(True))
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add(var.set_supports_auto(False))
 | 
			
		||||
        cg.add(var.set_supports_heat_cool(False))
 | 
			
		||||
 | 
			
		||||
    if CONF_COOL_ACTION in config:
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ void ThermostatClimate::setup() {
 | 
			
		||||
    restore->to_call(this).perform();
 | 
			
		||||
  } else {
 | 
			
		||||
    // restore from defaults, change_away handles temps for us
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    this->change_away_(false);
 | 
			
		||||
  }
 | 
			
		||||
  // refresh the climate action based on the restored settings
 | 
			
		||||
@@ -79,6 +79,7 @@ climate::ClimateTraits ThermostatClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_supports_auto_mode(this->supports_auto_);
 | 
			
		||||
  traits.set_supports_heat_cool_mode(this->supports_heat_cool_);
 | 
			
		||||
  traits.set_supports_cool_mode(this->supports_cool_);
 | 
			
		||||
  traits.set_supports_dry_mode(this->supports_dry_);
 | 
			
		||||
  traits.set_supports_fan_only_mode(this->supports_fan_only_);
 | 
			
		||||
@@ -130,7 +131,7 @@ climate::ClimateAction ThermostatClimate::compute_action_() {
 | 
			
		||||
      case climate::CLIMATE_MODE_OFF:
 | 
			
		||||
        target_action = climate::CLIMATE_ACTION_OFF;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      case climate::CLIMATE_MODE_COOL:
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
        if (this->supports_cool_) {
 | 
			
		||||
@@ -321,7 +322,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_OFF:
 | 
			
		||||
      trig = this->off_mode_trigger_;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      // trig = this->auto_mode_trigger_;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_COOL:
 | 
			
		||||
@@ -339,7 +340,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) {
 | 
			
		||||
    default:
 | 
			
		||||
      // we cannot report an invalid mode back to HA (even if it asked for one)
 | 
			
		||||
      //  and must assume some valid value
 | 
			
		||||
      mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
      mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
      // trig = this->auto_mode_trigger_;
 | 
			
		||||
  }
 | 
			
		||||
  assert(trig != nullptr);
 | 
			
		||||
@@ -434,6 +435,9 @@ ThermostatClimate::ThermostatClimate()
 | 
			
		||||
      swing_mode_vertical_trigger_(new Trigger<>()) {}
 | 
			
		||||
void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; }
 | 
			
		||||
void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
 | 
			
		||||
void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) {
 | 
			
		||||
  this->supports_heat_cool_ = supports_heat_cool;
 | 
			
		||||
}
 | 
			
		||||
void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; }
 | 
			
		||||
void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
 | 
			
		||||
void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; }
 | 
			
		||||
@@ -521,6 +525,7 @@ void ThermostatClimate::dump_config() {
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Hysteresis: %.1f°C", this->hysteresis_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Supports AUTO: %s", YESNO(this->supports_auto_));
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Supports HEAT/COOL: %s", YESNO(this->supports_heat_cool_));
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Supports COOL: %s", YESNO(this->supports_cool_));
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Supports DRY: %s", YESNO(this->supports_dry_));
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_));
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ class ThermostatClimate : public climate::Climate, public Component {
 | 
			
		||||
  void set_hysteresis(float hysteresis);
 | 
			
		||||
  void set_sensor(sensor::Sensor *sensor);
 | 
			
		||||
  void set_supports_auto(bool supports_auto);
 | 
			
		||||
  void set_supports_heat_cool(bool supports_heat_cool);
 | 
			
		||||
  void set_supports_cool(bool supports_cool);
 | 
			
		||||
  void set_supports_dry(bool supports_dry);
 | 
			
		||||
  void set_supports_fan_only(bool supports_fan_only);
 | 
			
		||||
@@ -113,6 +114,7 @@ class ThermostatClimate : public climate::Climate, public Component {
 | 
			
		||||
  /// A false value for any given attribute means that the controller has no such action
 | 
			
		||||
  /// (for example a thermostat, where only heating and not-heating is possible).
 | 
			
		||||
  bool supports_auto_{false};
 | 
			
		||||
  bool supports_heat_cool_{false};
 | 
			
		||||
  bool supports_cool_{false};
 | 
			
		||||
  bool supports_dry_{false};
 | 
			
		||||
  bool supports_fan_only_{false};
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,7 @@ void ToshibaClimate::transmit_state() {
 | 
			
		||||
      mode = TOSHIBA_MODE_COOL;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
    default:
 | 
			
		||||
      mode = TOSHIBA_MODE_AUTO;
 | 
			
		||||
  }
 | 
			
		||||
@@ -190,7 +190,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
    case TOSHIBA_MODE_AUTO:
 | 
			
		||||
    default:
 | 
			
		||||
      /* Note: Dry and Fan-only modes are reported as Auto, as they are not supported yet */
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Get the target temperature */
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ void TuyaClimate::setup() {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_OFF;
 | 
			
		||||
      if (datapoint.value_bool) {
 | 
			
		||||
        if (this->supports_heat_ && this->supports_cool_) {
 | 
			
		||||
          this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
          this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
        } else if (this->supports_heat_) {
 | 
			
		||||
          this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
        } else if (this->supports_cool_) {
 | 
			
		||||
 
 | 
			
		||||
@@ -333,7 +333,7 @@ void Tuya::send_raw_command_(TuyaCommand command) {
 | 
			
		||||
void Tuya::process_command_queue_() {
 | 
			
		||||
  uint32_t delay = millis() - this->last_command_timestamp_;
 | 
			
		||||
  // Left check of delay since last command in case theres ever a command sent by calling send_raw_command_ directly
 | 
			
		||||
  if (delay > COMMAND_DELAY && !command_queue_.empty()) {
 | 
			
		||||
  if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty()) {
 | 
			
		||||
    this->send_raw_command_(command_queue_.front());
 | 
			
		||||
    this->command_queue_.erase(command_queue_.begin());
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome import pins, automation
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BAUD_RATE,
 | 
			
		||||
@@ -92,42 +95,6 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_parity(config[CONF_PARITY]))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_device(
 | 
			
		||||
    name, config, item_config, baud_rate=None, require_tx=True, require_rx=True
 | 
			
		||||
):
 | 
			
		||||
    if not hasattr(config, "uart_devices"):
 | 
			
		||||
        config.uart_devices = {}
 | 
			
		||||
    devices = config.uart_devices
 | 
			
		||||
 | 
			
		||||
    uart_config = config.get_config_by_id(item_config[CONF_UART_ID])
 | 
			
		||||
 | 
			
		||||
    uart_id = uart_config[CONF_ID]
 | 
			
		||||
    device = devices.setdefault(uart_id, {})
 | 
			
		||||
 | 
			
		||||
    if require_tx:
 | 
			
		||||
        if CONF_TX_PIN not in uart_config:
 | 
			
		||||
            raise ValueError(f"Component {name} requires parent uart to declare tx_pin")
 | 
			
		||||
        if CONF_TX_PIN in device:
 | 
			
		||||
            raise ValueError(
 | 
			
		||||
                f"Component {name} cannot use the same uart.{CONF_TX_PIN} as component {device[CONF_TX_PIN]} is already using it"
 | 
			
		||||
            )
 | 
			
		||||
        device[CONF_TX_PIN] = name
 | 
			
		||||
 | 
			
		||||
    if require_rx:
 | 
			
		||||
        if CONF_RX_PIN not in uart_config:
 | 
			
		||||
            raise ValueError(f"Component {name} requires parent uart to declare rx_pin")
 | 
			
		||||
        if CONF_RX_PIN in device:
 | 
			
		||||
            raise ValueError(
 | 
			
		||||
                f"Component {name} cannot use the same uart.{CONF_RX_PIN} as component {device[CONF_RX_PIN]} is already using it"
 | 
			
		||||
            )
 | 
			
		||||
        device[CONF_RX_PIN] = name
 | 
			
		||||
 | 
			
		||||
    if baud_rate and uart_config[CONF_BAUD_RATE] != baud_rate:
 | 
			
		||||
        raise ValueError(
 | 
			
		||||
            f"Component {name} requires parent uart baud rate be {baud_rate}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# A schema to use for all UART devices, all UART integrations must extend this!
 | 
			
		||||
UART_DEVICE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
@@ -135,6 +102,64 @@ UART_DEVICE_SCHEMA = cv.Schema(
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
KEY_UART_DEVICES = "uart_devices"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validate_device_schema(
 | 
			
		||||
    name: str,
 | 
			
		||||
    *,
 | 
			
		||||
    baud_rate: Optional[int] = None,
 | 
			
		||||
    require_tx: bool = False,
 | 
			
		||||
    require_rx: bool = False,
 | 
			
		||||
):
 | 
			
		||||
    def validate_baud_rate(value):
 | 
			
		||||
        if value != baud_rate:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"Component {name} required baud rate {baud_rate} for the uart bus"
 | 
			
		||||
            )
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    def validate_pin(opt, device):
 | 
			
		||||
        def validator(value):
 | 
			
		||||
            if opt in device:
 | 
			
		||||
                raise cv.Invalid(
 | 
			
		||||
                    f"The uart {opt} is used both by {name} and {device[opt]}, "
 | 
			
		||||
                    f"but can only be used by one. Please create a new uart bus for {name}."
 | 
			
		||||
                )
 | 
			
		||||
            device[opt] = name
 | 
			
		||||
            return value
 | 
			
		||||
 | 
			
		||||
        return validator
 | 
			
		||||
 | 
			
		||||
    def validate_hub(hub_config):
 | 
			
		||||
        hub_schema = {}
 | 
			
		||||
        uart_id = hub_config[CONF_ID]
 | 
			
		||||
        devices = fv.full_config.get().data.setdefault(KEY_UART_DEVICES, {})
 | 
			
		||||
        device = devices.setdefault(uart_id, {})
 | 
			
		||||
 | 
			
		||||
        if require_tx:
 | 
			
		||||
            hub_schema[
 | 
			
		||||
                cv.Required(
 | 
			
		||||
                    CONF_TX_PIN,
 | 
			
		||||
                    msg=f"Component {name} requires this uart bus to declare a tx_pin",
 | 
			
		||||
                )
 | 
			
		||||
            ] = validate_pin(CONF_TX_PIN, device)
 | 
			
		||||
        if require_rx:
 | 
			
		||||
            hub_schema[
 | 
			
		||||
                cv.Required(
 | 
			
		||||
                    CONF_RX_PIN,
 | 
			
		||||
                    msg=f"Component {name} requires this uart bus to declare a rx_pin",
 | 
			
		||||
                )
 | 
			
		||||
            ] = validate_pin(CONF_RX_PIN, device)
 | 
			
		||||
        if baud_rate is not None:
 | 
			
		||||
            hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate
 | 
			
		||||
        return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config)
 | 
			
		||||
 | 
			
		||||
    return cv.Schema(
 | 
			
		||||
        {cv.Required(CONF_UART_ID): fv.id_declaration_match_schema(validate_hub)},
 | 
			
		||||
        extra=cv.ALLOW_EXTRA,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_uart_device(var, config):
 | 
			
		||||
    """Register a UART device, setting up all the internal values.
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ void WhirlpoolClimate::transmit_state() {
 | 
			
		||||
    this->powered_on_assumed = powered_on;
 | 
			
		||||
  }
 | 
			
		||||
  switch (this->mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      // set fan auto
 | 
			
		||||
      // set temp auto temp
 | 
			
		||||
      // set sleep false
 | 
			
		||||
@@ -239,7 +239,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
        break;
 | 
			
		||||
      case WHIRLPOOL_AUTO:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_AUTO;
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.automation import Condition
 | 
			
		||||
from esphome.components.network import add_mdns_library
 | 
			
		||||
@@ -137,6 +138,19 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validate(config):
 | 
			
		||||
    has_sta = bool(config.get(CONF_NETWORKS, True))
 | 
			
		||||
    has_ap = CONF_AP in config
 | 
			
		||||
    has_improv = "esp32_improv" in fv.full_config.get()
 | 
			
		||||
    if (not has_sta) and (not has_ap) and (not has_improv):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please specify at least an SSID or an Access Point to create."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate(config):
 | 
			
		||||
    if CONF_PASSWORD in config and CONF_SSID not in config:
 | 
			
		||||
        raise cv.Invalid("Cannot have WiFi password without SSID!")
 | 
			
		||||
@@ -157,9 +171,7 @@ def _validate(config):
 | 
			
		||||
        config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
 | 
			
		||||
 | 
			
		||||
    if (CONF_NETWORKS not in config) and (CONF_AP not in config):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please specify at least an SSID or an Access Point " "to create."
 | 
			
		||||
        )
 | 
			
		||||
        config[CONF_NETWORKS] = []
 | 
			
		||||
 | 
			
		||||
    if config.get(CONF_FAST_CONNECT, False):
 | 
			
		||||
        networks = config.get(CONF_NETWORKS, [])
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,8 @@ void WiFiComponent::setup() {
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_IMPROV
 | 
			
		||||
  if (esp32_improv::global_improv_component != nullptr)
 | 
			
		||||
    esp32_improv::global_improv_component->start();
 | 
			
		||||
    if (this->wifi_mode_(true, {}))
 | 
			
		||||
      esp32_improv::global_improv_component->start();
 | 
			
		||||
#endif
 | 
			
		||||
  this->wifi_apply_hostname_();
 | 
			
		||||
#if defined(ARDUINO_ARCH_ESP32) && defined(USE_MDNS)
 | 
			
		||||
@@ -143,11 +144,11 @@ void WiFiComponent::loop() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_IMPROV
 | 
			
		||||
    if (esp32_improv::global_improv_component != nullptr) {
 | 
			
		||||
      if (!this->is_connected()) {
 | 
			
		||||
        esp32_improv::global_improv_component->start();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (esp32_improv::global_improv_component != nullptr)
 | 
			
		||||
      if (!this->is_connected())
 | 
			
		||||
        if (this->wifi_mode_(true, {}))
 | 
			
		||||
          esp32_improv::global_improv_component->start();
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    if (!this->has_ap() && this->reboot_timeout_ != 0) {
 | 
			
		||||
@@ -586,7 +587,7 @@ void WiFiComponent::retry_connect() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool WiFiComponent::can_proceed() {
 | 
			
		||||
  if (this->has_ap() && !this->has_sta()) {
 | 
			
		||||
  if (!this->has_sta()) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return this->is_connected();
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,7 @@ const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000;
 | 
			
		||||
climate::ClimateTraits YashimaClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.set_supports_current_temperature(this->sensor_ != nullptr);
 | 
			
		||||
  traits.set_supports_auto_mode(true);
 | 
			
		||||
  traits.set_supports_heat_cool_mode(true);
 | 
			
		||||
  traits.set_supports_cool_mode(this->supports_cool_);
 | 
			
		||||
  traits.set_supports_heat_mode(this->supports_heat_);
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(false);
 | 
			
		||||
@@ -139,7 +139,7 @@ void YashimaClimate::transmit_state_() {
 | 
			
		||||
 | 
			
		||||
  // Set mode
 | 
			
		||||
  switch (this->mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      remote_state[0] |= YASHIMA_MODE_AUTO_BYTE0;
 | 
			
		||||
      remote_state[5] |= YASHIMA_MODE_AUTO_BYTE5;
 | 
			
		||||
      break;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,13 @@ from esphome.helpers import indent
 | 
			
		||||
from esphome.util import safe_print, OrderedDict
 | 
			
		||||
 | 
			
		||||
from typing import List, Optional, Tuple, Union
 | 
			
		||||
from esphome.core import ConfigType
 | 
			
		||||
from esphome.loader import get_component, get_platform, ComponentManifest
 | 
			
		||||
from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue
 | 
			
		||||
from esphome.voluptuous_schema import ExtraKeysInvalid
 | 
			
		||||
from esphome.log import color, Fore
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.types import ConfigType, ConfigPathType, ConfigFragmentType
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@@ -54,7 +56,7 @@ def _path_begins_with(path, other):  # type: (ConfigPath, ConfigPath) -> bool
 | 
			
		||||
    return path[: len(other)] == other
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Config(OrderedDict):
 | 
			
		||||
class Config(OrderedDict, fv.FinalValidateConfig):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        # A list of voluptuous errors
 | 
			
		||||
@@ -65,6 +67,7 @@ class Config(OrderedDict):
 | 
			
		||||
        self.output_paths = []  # type: List[Tuple[ConfigPath, str]]
 | 
			
		||||
        # A list of components ids with the config path
 | 
			
		||||
        self.declare_ids = []  # type: List[Tuple[core.ID, ConfigPath]]
 | 
			
		||||
        self._data = {}
 | 
			
		||||
 | 
			
		||||
    def add_error(self, error):
 | 
			
		||||
        # type: (vol.Invalid) -> None
 | 
			
		||||
@@ -72,6 +75,12 @@ class Config(OrderedDict):
 | 
			
		||||
            for err in error.errors:
 | 
			
		||||
                self.add_error(err)
 | 
			
		||||
            return
 | 
			
		||||
        if cv.ROOT_CONFIG_PATH in error.path:
 | 
			
		||||
            # Root value means that the path before the root should be ignored
 | 
			
		||||
            last_root = max(
 | 
			
		||||
                i for i, v in enumerate(error.path) if v is cv.ROOT_CONFIG_PATH
 | 
			
		||||
            )
 | 
			
		||||
            error.path = error.path[last_root + 1 :]
 | 
			
		||||
        self.errors.append(error)
 | 
			
		||||
 | 
			
		||||
    @contextmanager
 | 
			
		||||
@@ -140,13 +149,16 @@ class Config(OrderedDict):
 | 
			
		||||
 | 
			
		||||
        return doc_range
 | 
			
		||||
 | 
			
		||||
    def get_nested_item(self, path):
 | 
			
		||||
        # type: (ConfigPath) -> ConfigType
 | 
			
		||||
    def get_nested_item(
 | 
			
		||||
        self, path: ConfigPathType, raise_error: bool = False
 | 
			
		||||
    ) -> ConfigFragmentType:
 | 
			
		||||
        data = self
 | 
			
		||||
        for item_index in path:
 | 
			
		||||
            try:
 | 
			
		||||
                data = data[item_index]
 | 
			
		||||
            except (KeyError, IndexError, TypeError):
 | 
			
		||||
                if raise_error:
 | 
			
		||||
                    raise
 | 
			
		||||
                return {}
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
@@ -163,11 +175,20 @@ class Config(OrderedDict):
 | 
			
		||||
            part.append(item_index)
 | 
			
		||||
        return part
 | 
			
		||||
 | 
			
		||||
    def get_config_by_id(self, id):
 | 
			
		||||
    def get_path_for_id(self, id: core.ID):
 | 
			
		||||
        """Return the config fragment where the given ID is declared."""
 | 
			
		||||
        for declared_id, path in self.declare_ids:
 | 
			
		||||
            if declared_id.id == str(id):
 | 
			
		||||
                return self.get_nested_item(path[:-1])
 | 
			
		||||
        return None
 | 
			
		||||
                return path
 | 
			
		||||
        raise KeyError(f"ID {id} not found in configuration")
 | 
			
		||||
 | 
			
		||||
    def get_config_for_path(self, path: ConfigPathType) -> ConfigFragmentType:
 | 
			
		||||
        return self.get_nested_item(path, raise_error=True)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def data(self):
 | 
			
		||||
        """Return temporary data used by final validation functions."""
 | 
			
		||||
        return self._data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def iter_ids(config, path=None):
 | 
			
		||||
@@ -189,23 +210,22 @@ def do_id_pass(result):  # type: (Config) -> None
 | 
			
		||||
    from esphome.cpp_generator import MockObjClass
 | 
			
		||||
    from esphome.cpp_types import Component
 | 
			
		||||
 | 
			
		||||
    declare_ids = result.declare_ids  # type: List[Tuple[core.ID, ConfigPath]]
 | 
			
		||||
    searching_ids = []  # type: List[Tuple[core.ID, ConfigPath]]
 | 
			
		||||
    for id, path in iter_ids(result):
 | 
			
		||||
        if id.is_declaration:
 | 
			
		||||
            if id.id is not None:
 | 
			
		||||
                # Look for duplicate definitions
 | 
			
		||||
                match = next((v for v in declare_ids if v[0].id == id.id), None)
 | 
			
		||||
                match = next((v for v in result.declare_ids if v[0].id == id.id), None)
 | 
			
		||||
                if match is not None:
 | 
			
		||||
                    opath = "->".join(str(v) for v in match[1])
 | 
			
		||||
                    result.add_str_error(f"ID {id.id} redefined! Check {opath}", path)
 | 
			
		||||
                    continue
 | 
			
		||||
            declare_ids.append((id, path))
 | 
			
		||||
            result.declare_ids.append((id, path))
 | 
			
		||||
        else:
 | 
			
		||||
            searching_ids.append((id, path))
 | 
			
		||||
    # Resolve default ids after manual IDs
 | 
			
		||||
    for id, _ in declare_ids:
 | 
			
		||||
        id.resolve([v[0].id for v in declare_ids])
 | 
			
		||||
    for id, _ in result.declare_ids:
 | 
			
		||||
        id.resolve([v[0].id for v in result.declare_ids])
 | 
			
		||||
        if isinstance(id.type, MockObjClass) and id.type.inherits_from(Component):
 | 
			
		||||
            CORE.component_ids.add(id.id)
 | 
			
		||||
 | 
			
		||||
@@ -213,7 +233,7 @@ def do_id_pass(result):  # type: (Config) -> None
 | 
			
		||||
    for id, path in searching_ids:
 | 
			
		||||
        if id.id is not None:
 | 
			
		||||
            # manually declared
 | 
			
		||||
            match = next((v[0] for v in declare_ids if v[0].id == id.id), None)
 | 
			
		||||
            match = next((v[0] for v in result.declare_ids if v[0].id == id.id), None)
 | 
			
		||||
            if match is None or not match.is_manual:
 | 
			
		||||
                # No declared ID with this name
 | 
			
		||||
                import difflib
 | 
			
		||||
@@ -224,7 +244,7 @@ def do_id_pass(result):  # type: (Config) -> None
 | 
			
		||||
                )
 | 
			
		||||
                # Find candidates
 | 
			
		||||
                matches = difflib.get_close_matches(
 | 
			
		||||
                    id.id, [v[0].id for v in declare_ids if v[0].is_manual]
 | 
			
		||||
                    id.id, [v[0].id for v in result.declare_ids if v[0].is_manual]
 | 
			
		||||
                )
 | 
			
		||||
                if matches:
 | 
			
		||||
                    matches_s = ", ".join(f'"{x}"' for x in matches)
 | 
			
		||||
@@ -245,7 +265,7 @@ def do_id_pass(result):  # type: (Config) -> None
 | 
			
		||||
 | 
			
		||||
        if id.id is None and id.type is not None:
 | 
			
		||||
            matches = []
 | 
			
		||||
            for v in declare_ids:
 | 
			
		||||
            for v in result.declare_ids:
 | 
			
		||||
                if v[0] is None or not isinstance(v[0].type, MockObjClass):
 | 
			
		||||
                    continue
 | 
			
		||||
                inherits = v[0].type.inherits_from(id.type)
 | 
			
		||||
@@ -278,8 +298,6 @@ def do_id_pass(result):  # type: (Config) -> None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def recursive_check_replaceme(value):
 | 
			
		||||
    import esphome.config_validation as cv
 | 
			
		||||
 | 
			
		||||
    if isinstance(value, list):
 | 
			
		||||
        return cv.Schema([recursive_check_replaceme])(value)
 | 
			
		||||
    if isinstance(value, dict):
 | 
			
		||||
@@ -558,14 +576,16 @@ def validate_config(config, command_line_substitutions):
 | 
			
		||||
    # 7. Final validation
 | 
			
		||||
    if not result.errors:
 | 
			
		||||
        # Inter - components validation
 | 
			
		||||
        for path, conf, comp in validate_queue:
 | 
			
		||||
            if comp.config_schema is None:
 | 
			
		||||
        token = fv.full_config.set(result)
 | 
			
		||||
 | 
			
		||||
        for path, _, comp in validate_queue:
 | 
			
		||||
            if comp.final_validate_schema is None:
 | 
			
		||||
                continue
 | 
			
		||||
            if callable(comp.validate):
 | 
			
		||||
                try:
 | 
			
		||||
                    comp.validate(result, result.get_nested_item(path))
 | 
			
		||||
                except ValueError as err:
 | 
			
		||||
                    result.add_str_error(err, path)
 | 
			
		||||
            conf = result.get_nested_item(path)
 | 
			
		||||
            with result.catch_error(path):
 | 
			
		||||
                comp.final_validate_schema(conf)
 | 
			
		||||
 | 
			
		||||
        fv.full_config.reset(token)
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
@@ -621,8 +641,12 @@ def _format_vol_invalid(ex, config):
 | 
			
		||||
            )
 | 
			
		||||
    elif "extra keys not allowed" in str(ex):
 | 
			
		||||
        message += "[{}] is an invalid option for [{}].".format(ex.path[-1], paren)
 | 
			
		||||
    elif "required key not provided" in str(ex):
 | 
			
		||||
        message += "'{}' is a required option for [{}].".format(ex.path[-1], paren)
 | 
			
		||||
    elif isinstance(ex, vol.RequiredFieldInvalid):
 | 
			
		||||
        if ex.msg == "required key not provided":
 | 
			
		||||
            message += "'{}' is a required option for [{}].".format(ex.path[-1], paren)
 | 
			
		||||
        else:
 | 
			
		||||
            # Required has set a custom error message
 | 
			
		||||
            message += ex.msg
 | 
			
		||||
    else:
 | 
			
		||||
        message += humanize_error(config, ex)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,9 @@ Inclusive = vol.Inclusive
 | 
			
		||||
ALLOW_EXTRA = vol.ALLOW_EXTRA
 | 
			
		||||
UNDEFINED = vol.UNDEFINED
 | 
			
		||||
RequiredFieldInvalid = vol.RequiredFieldInvalid
 | 
			
		||||
# this sentinel object can be placed in an 'Invalid' path to say
 | 
			
		||||
# the rest of the error path is relative to the root config path
 | 
			
		||||
ROOT_CONFIG_PATH = object()
 | 
			
		||||
 | 
			
		||||
RESERVED_IDS = [
 | 
			
		||||
    # C++ keywords http://en.cppreference.com/w/cpp/keyword
 | 
			
		||||
@@ -218,8 +221,8 @@ class Required(vol.Required):
 | 
			
		||||
    - *not* the `config.get(CONF_<KEY>)` syntax.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, key):
 | 
			
		||||
        super().__init__(key)
 | 
			
		||||
    def __init__(self, key, msg=None):
 | 
			
		||||
        super().__init__(key, msg=msg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_not_templatable(value):
 | 
			
		||||
@@ -1073,6 +1076,7 @@ def invalid(message):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def valid(value):
 | 
			
		||||
    """A validator that is always valid and returns the value as-is."""
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
MAJOR_VERSION = 1
 | 
			
		||||
MINOR_VERSION = 19
 | 
			
		||||
PATCH_VERSION = "0b2"
 | 
			
		||||
PATCH_VERSION = "3"
 | 
			
		||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
 | 
			
		||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import logging
 | 
			
		||||
import math
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
 | 
			
		||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ARDUINO_VERSION,
 | 
			
		||||
@@ -23,6 +23,7 @@ from esphome.util import OrderedDict
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from ..cpp_generator import MockObj, MockObjClass, Statement
 | 
			
		||||
    from ..types import ConfigType
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@@ -462,9 +463,9 @@ class EsphomeCore:
 | 
			
		||||
        # The board that's used (for example nodemcuv2)
 | 
			
		||||
        self.board: Optional[str] = None
 | 
			
		||||
        # The full raw configuration
 | 
			
		||||
        self.raw_config: Optional[ConfigType] = None
 | 
			
		||||
        self.raw_config: Optional["ConfigType"] = None
 | 
			
		||||
        # The validated configuration, this is None until the config has been validated
 | 
			
		||||
        self.config: Optional[ConfigType] = None
 | 
			
		||||
        self.config: Optional["ConfigType"] = None
 | 
			
		||||
        # The pending tasks in the task queue (mostly for C++ generation)
 | 
			
		||||
        # This is a priority queue (with heapq)
 | 
			
		||||
        # Each item is a tuple of form: (-priority, unique number, task)
 | 
			
		||||
@@ -752,6 +753,3 @@ class EnumValue:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CORE = EsphomeCore()
 | 
			
		||||
 | 
			
		||||
ConfigType = Dict[str, Any]
 | 
			
		||||
CoreType = EsphomeCore
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ namespace esphome {
 | 
			
		||||
class Application {
 | 
			
		||||
 public:
 | 
			
		||||
  void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) {
 | 
			
		||||
    this->name_add_mac_suffix_ = name_add_mac_suffix;
 | 
			
		||||
    if (name_add_mac_suffix) {
 | 
			
		||||
      this->name_ = name + "-" + get_mac_address().substr(6);
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -97,6 +98,8 @@ class Application {
 | 
			
		||||
  /// Get the name of this Application set by set_name().
 | 
			
		||||
  const std::string &get_name() const { return this->name_; }
 | 
			
		||||
 | 
			
		||||
  bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
 | 
			
		||||
 | 
			
		||||
  const std::string &get_compilation_time() const { return this->compilation_time_; }
 | 
			
		||||
 | 
			
		||||
  /** Set the target interval with which to run the loop() calls.
 | 
			
		||||
@@ -245,6 +248,7 @@ class Application {
 | 
			
		||||
 | 
			
		||||
  std::string name_;
 | 
			
		||||
  std::string compilation_time_;
 | 
			
		||||
  bool name_add_mac_suffix_;
 | 
			
		||||
  uint32_t last_loop_{0};
 | 
			
		||||
  uint32_t loop_interval_{16};
 | 
			
		||||
  int dump_config_at_{-1};
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@
 | 
			
		||||
#define USE_JSON
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#define USE_ESP32_CAMERA
 | 
			
		||||
#define USE_ESP32_BLE_SERVER
 | 
			
		||||
#define USE_IMPROV
 | 
			
		||||
#endif
 | 
			
		||||
#define USE_TIME
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,8 @@ from esphome.const import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# pylint: disable=unused-import
 | 
			
		||||
from esphome.core import coroutine, ID, CORE, ConfigType
 | 
			
		||||
from esphome.core import coroutine, ID, CORE
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
from esphome.cpp_generator import RawExpression, add, get_variable
 | 
			
		||||
from esphome.cpp_types import App, GPIOPin
 | 
			
		||||
from esphome.util import Registry, RegistryEntry
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import json
 | 
			
		||||
import logging
 | 
			
		||||
import multiprocessing
 | 
			
		||||
import os
 | 
			
		||||
import secrets
 | 
			
		||||
import shutil
 | 
			
		||||
import subprocess
 | 
			
		||||
import threading
 | 
			
		||||
@@ -44,6 +45,8 @@ from esphome.zeroconf import DashboardStatus, Zeroconf
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
ENV_DEV = "ESPHOME_DASHBOARD_DEV"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DashboardSettings:
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
@@ -111,6 +114,7 @@ def template_args():
 | 
			
		||||
        docs_link = "https://next.esphome.io/"
 | 
			
		||||
    else:
 | 
			
		||||
        docs_link = "https://www.esphome.io/"
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "version": version,
 | 
			
		||||
        "docs_link": docs_link,
 | 
			
		||||
@@ -349,6 +353,7 @@ class SerialPortRequestHandler(BaseHandler):
 | 
			
		||||
            data.append({"port": port.path, "desc": desc})
 | 
			
		||||
        data.append({"port": "OTA", "desc": "Over-The-Air"})
 | 
			
		||||
        data.sort(key=lambda x: x["port"], reverse=True)
 | 
			
		||||
        self.set_header("content-type", "application/json")
 | 
			
		||||
        self.write(json.dumps(data))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -358,11 +363,15 @@ class WizardRequestHandler(BaseHandler):
 | 
			
		||||
        from esphome import wizard
 | 
			
		||||
 | 
			
		||||
        kwargs = {
 | 
			
		||||
            k: "".join(x.decode() for x in v) for k, v in self.request.arguments.items()
 | 
			
		||||
            k: "".join(x.decode() for x in v)
 | 
			
		||||
            for k, v in self.request.arguments.items()
 | 
			
		||||
            if k in ("name", "platform", "board", "ssid", "psk", "password")
 | 
			
		||||
        }
 | 
			
		||||
        kwargs["ota_password"] = secrets.token_hex(16)
 | 
			
		||||
        destination = settings.rel_path(kwargs["name"] + ".yaml")
 | 
			
		||||
        wizard.wizard_write(path=destination, **kwargs)
 | 
			
		||||
        self.redirect("./?begin=True")
 | 
			
		||||
        self.set_status(200)
 | 
			
		||||
        self.finish()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DownloadBinaryRequestHandler(BaseHandler):
 | 
			
		||||
@@ -473,7 +482,7 @@ class MainRequestHandler(BaseHandler):
 | 
			
		||||
        entries = _list_dashboard_entries()
 | 
			
		||||
 | 
			
		||||
        self.render(
 | 
			
		||||
            "templates/index.html",
 | 
			
		||||
            get_template_path("index"),
 | 
			
		||||
            entries=entries,
 | 
			
		||||
            begin=begin,
 | 
			
		||||
            **template_args(),
 | 
			
		||||
@@ -560,6 +569,7 @@ class PingRequestHandler(BaseHandler):
 | 
			
		||||
    @authenticated
 | 
			
		||||
    def get(self):
 | 
			
		||||
        PING_REQUEST.set()
 | 
			
		||||
        self.set_header("content-type", "application/json")
 | 
			
		||||
        self.write(json.dumps(PING_RESULT))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -567,6 +577,21 @@ def is_allowed(configuration):
 | 
			
		||||
    return os.path.sep not in configuration
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InfoRequestHandler(BaseHandler):
 | 
			
		||||
    @authenticated
 | 
			
		||||
    @bind_config
 | 
			
		||||
    def get(self, configuration=None):
 | 
			
		||||
        yaml_path = settings.rel_path(configuration)
 | 
			
		||||
        all_yaml_files = settings.list_yaml_files()
 | 
			
		||||
 | 
			
		||||
        if yaml_path not in all_yaml_files:
 | 
			
		||||
            self.set_status(404)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.set_header("content-type", "application/json")
 | 
			
		||||
        self.write(DashboardEntry(yaml_path).storage.to_json())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EditRequestHandler(BaseHandler):
 | 
			
		||||
    @authenticated
 | 
			
		||||
    @bind_config
 | 
			
		||||
@@ -633,7 +658,7 @@ class LoginHandler(BaseHandler):
 | 
			
		||||
 | 
			
		||||
    def render_login_page(self, error=None):
 | 
			
		||||
        self.render(
 | 
			
		||||
            "templates/login.html",
 | 
			
		||||
            get_template_path("login"),
 | 
			
		||||
            error=error,
 | 
			
		||||
            hassio=settings.using_hassio_auth,
 | 
			
		||||
            has_username=bool(settings.username),
 | 
			
		||||
@@ -691,22 +716,48 @@ class LogoutHandler(BaseHandler):
 | 
			
		||||
        self.redirect("./login")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_STATIC_FILE_HASHES = {}
 | 
			
		||||
def get_base_frontend_path():
 | 
			
		||||
    if ENV_DEV not in os.environ:
 | 
			
		||||
        import esphome_dashboard
 | 
			
		||||
 | 
			
		||||
        return esphome_dashboard.where()
 | 
			
		||||
 | 
			
		||||
    static_path = os.environ[ENV_DEV]
 | 
			
		||||
    if not static_path.endswith("/"):
 | 
			
		||||
        static_path += "/"
 | 
			
		||||
 | 
			
		||||
    # This path can be relative, so resolve against the root or else templates don't work
 | 
			
		||||
    return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_template_path(template_name):
 | 
			
		||||
    return os.path.join(get_base_frontend_path(), f"{template_name}.template.html")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_static_path(*args):
 | 
			
		||||
    return os.path.join(get_base_frontend_path(), "static", *args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@functools.lru_cache(maxsize=None)
 | 
			
		||||
def get_static_file_url(name):
 | 
			
		||||
    static_path = os.path.join(os.path.dirname(__file__), "static")
 | 
			
		||||
    if name in _STATIC_FILE_HASHES:
 | 
			
		||||
        hash_ = _STATIC_FILE_HASHES[name]
 | 
			
		||||
    else:
 | 
			
		||||
        path = os.path.join(static_path, name)
 | 
			
		||||
        with open(path, "rb") as f_handle:
 | 
			
		||||
            hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8]
 | 
			
		||||
        _STATIC_FILE_HASHES[name] = hash_
 | 
			
		||||
    return f"./static/{name}?hash={hash_}"
 | 
			
		||||
    base = f"./static/{name}"
 | 
			
		||||
 | 
			
		||||
    if ENV_DEV in os.environ:
 | 
			
		||||
        return base
 | 
			
		||||
 | 
			
		||||
    # Module imports can't deduplicate if stuff added to url
 | 
			
		||||
    if name == "js/esphome/index.js":
 | 
			
		||||
        import esphome_dashboard
 | 
			
		||||
 | 
			
		||||
        return base.replace("index.js", esphome_dashboard.entrypoint())
 | 
			
		||||
 | 
			
		||||
    path = get_static_path(name)
 | 
			
		||||
    with open(path, "rb") as f_handle:
 | 
			
		||||
        hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8]
 | 
			
		||||
    return f"{base}?hash={hash_}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def make_app(debug=False):
 | 
			
		||||
def make_app(debug=get_bool_env(ENV_DEV)):
 | 
			
		||||
    def log_function(handler):
 | 
			
		||||
        if handler.get_status() < 400:
 | 
			
		||||
            log_method = access_log.info
 | 
			
		||||
@@ -736,7 +787,6 @@ def make_app(debug=False):
 | 
			
		||||
                    "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    static_path = os.path.join(os.path.dirname(__file__), "static")
 | 
			
		||||
    app_settings = {
 | 
			
		||||
        "debug": debug,
 | 
			
		||||
        "cookie_secret": settings.cookie_secret,
 | 
			
		||||
@@ -758,6 +808,7 @@ def make_app(debug=False):
 | 
			
		||||
            (rel + "vscode", EsphomeVscodeHandler),
 | 
			
		||||
            (rel + "ace", EsphomeAceEditorHandler),
 | 
			
		||||
            (rel + "update-all", EsphomeUpdateAllHandler),
 | 
			
		||||
            (rel + "info", InfoRequestHandler),
 | 
			
		||||
            (rel + "edit", EditRequestHandler),
 | 
			
		||||
            (rel + "download.bin", DownloadBinaryRequestHandler),
 | 
			
		||||
            (rel + "serial-ports", SerialPortRequestHandler),
 | 
			
		||||
@@ -765,14 +816,11 @@ def make_app(debug=False):
 | 
			
		||||
            (rel + "delete", DeleteRequestHandler),
 | 
			
		||||
            (rel + "undo-delete", UndoDeleteRequestHandler),
 | 
			
		||||
            (rel + "wizard.html", WizardRequestHandler),
 | 
			
		||||
            (rel + r"static/(.*)", StaticFileHandler, {"path": static_path}),
 | 
			
		||||
            (rel + r"static/(.*)", StaticFileHandler, {"path": get_static_path()}),
 | 
			
		||||
        ],
 | 
			
		||||
        **app_settings,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if debug:
 | 
			
		||||
        _STATIC_FILE_HASHES.clear()
 | 
			
		||||
 | 
			
		||||
    return app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,398 +0,0 @@
 | 
			
		||||
/* Base */
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  /* Colors */
 | 
			
		||||
  --primary-bg-color: #fafafa;
 | 
			
		||||
 | 
			
		||||
  --alert-standard-color: #666666;
 | 
			
		||||
  --alert-standard-color-bg: #e6e6e6;
 | 
			
		||||
  --alert-info-color: #00539f;
 | 
			
		||||
  --alert-info-color-bg: #E6EEF5;
 | 
			
		||||
  --alert-success-color: #4CAF50;
 | 
			
		||||
  --alert-success-color-bg: #EDF7EE;
 | 
			
		||||
  --alert-warning-color: #FF9800;
 | 
			
		||||
  --alert-warning-color-bg: #FFF5E6;
 | 
			
		||||
  --alert-error-color: #D93025;
 | 
			
		||||
  --alert-error-color-bg: #FAEFEB;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  background-color: var(--primary-bg-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Layout */
 | 
			
		||||
.valign-wrapper {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width:100vw;
 | 
			
		||||
  height:100vh;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.valign {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main {
 | 
			
		||||
  flex: 1 0 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Alerts & Errors */
 | 
			
		||||
.alert {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 10px auto;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  border-left-width: 4px;
 | 
			
		||||
  border-left-style: solid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.alert .title {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.alert .title::after {
 | 
			
		||||
  content: "\A";
 | 
			
		||||
  white-space: pre;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.alert.alert-error {
 | 
			
		||||
  color: var(--alert-error-color);
 | 
			
		||||
  border-left-color: var(--alert-error-color);
 | 
			
		||||
  background-color: var(--alert-error-color-bg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card.card-error, .card.status-offline {
 | 
			
		||||
  border-top: 4px solid var(--alert-error-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card.status-online {
 | 
			
		||||
  border-top: 4px solid var(--alert-success-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card.status-not-responding {
 | 
			
		||||
  border-top: 4px solid var(--alert-warning-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card.status-unknown {
 | 
			
		||||
  border-top: 4px solid var(--alert-standard-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Login Page */
 | 
			
		||||
#login-page .row.no-bottom-margin {
 | 
			
		||||
  margin-bottom: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page .logo {
 | 
			
		||||
  display: block;
 | 
			
		||||
  width: auto;
 | 
			
		||||
  max-width: 300px;
 | 
			
		||||
  margin-left: auto;
 | 
			
		||||
  margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page .input-field input:focus + label  {
 | 
			
		||||
  color: #000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page .input-field input:focus {
 | 
			
		||||
  border-bottom: 1px solid #000;
 | 
			
		||||
  box-shadow: 0 1px 0 0 #000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page .input-field .prefix.active {
 | 
			
		||||
  color: #000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page .version-number {
 | 
			
		||||
  display: block;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  margin-bottom: 20px;;
 | 
			
		||||
  color:#808080;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page footer {
 | 
			
		||||
  color: #757575;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page footer a {
 | 
			
		||||
  color: #424242;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page footer p {
 | 
			
		||||
  -webkit-margin-before: 0px;
 | 
			
		||||
  margin-block-start: 0px;
 | 
			
		||||
  -webkit-margin-after: 5px;
 | 
			
		||||
  margin-block-end: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#login-page footer p:last-child {
 | 
			
		||||
  -webkit-margin-after: 0px;
 | 
			
		||||
  margin-block-end: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Dashboard */
 | 
			
		||||
.logo-wrapper {
 | 
			
		||||
  height: 64px;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  width: 0;
 | 
			
		||||
  margin-left: 24px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.logo {
 | 
			
		||||
  width: auto;
 | 
			
		||||
  height: 48px;
 | 
			
		||||
  margin: 8px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 601px) {
 | 
			
		||||
  .logo {
 | 
			
		||||
    height: 38px;
 | 
			
		||||
    margin: 9px 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-icons {
 | 
			
		||||
  margin-right: 24px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-icons i {
 | 
			
		||||
  color: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
nav {
 | 
			
		||||
  height: auto;
 | 
			
		||||
  line-height: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.select-port-container {
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
  width: 350px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.serial-port-select {
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
  width: 350px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.serial-port-select .select-dropdown {
 | 
			
		||||
  color: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.serial-port-select .select-dropdown:focus {
 | 
			
		||||
  border-bottom: 1px solid #607d8b !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.serial-port-select .caret {
 | 
			
		||||
  fill: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.serial-port-select .dropdown-content li>span {
 | 
			
		||||
  color: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nav-dropdown li a,  .node-dropdown li a {
 | 
			
		||||
  color: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main .container {
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
  width: 90%;
 | 
			
		||||
  max-width: 1920px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nodes .card-content {
 | 
			
		||||
  height: calc(100% - 47px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nodes .card-content, #nodes .card-action  {
 | 
			
		||||
  padding: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nodes .grid-1-col {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nodes .grid-2-col {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  grid-column-gap: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nodes .grid-3-col {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr 1fr;
 | 
			
		||||
  grid-column-gap: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 1100px) {
 | 
			
		||||
  #nodes .grid-3-col {
 | 
			
		||||
    grid-template-columns: 1fr 1fr;
 | 
			
		||||
    grid-column-gap: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 750px) {
 | 
			
		||||
  #nodes .grid-2-col {
 | 
			
		||||
    grid-template-columns: 1fr;
 | 
			
		||||
    grid-column-gap: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #nodes .grid-3-col {
 | 
			
		||||
    grid-template-columns: 1fr;
 | 
			
		||||
    grid-column-gap: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
i.node-update-avaliable {
 | 
			
		||||
  color:#3f51b5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
i.node-webserver {
 | 
			
		||||
  color:#039be5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.node-config-path {
 | 
			
		||||
  margin-top: -8px;
 | 
			
		||||
  margin-bottom: 8px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.node-card-comment {
 | 
			
		||||
  color: #444;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card-action a, .card-dropdown-action a {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tooltipped {
 | 
			
		||||
  cursor: help;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#js-loading-indicator {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  transform: translate(-50%, -50%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.editor {
 | 
			
		||||
  margin-top: 0;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  height: calc(100% - 56px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.inlinecode {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  padding: 0.2em 0.4em;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  font-size: 85%;
 | 
			
		||||
  background-color: rgba(27,31,35,0.05);
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.log {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  max-height: calc(100% - 56px);
 | 
			
		||||
  background-color: #1c1c1c;
 | 
			
		||||
  margin-top: 0;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  padding: 16px;
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
  line-height: 1.45;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
  overflow-wrap: break-word;
 | 
			
		||||
  color: #DDD;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.log-bold { font-weight: bold; }
 | 
			
		||||
.log-italic { font-style: italic; }
 | 
			
		||||
.log-underline { text-decoration: underline; }
 | 
			
		||||
.log-strikethrough { text-decoration: line-through; }
 | 
			
		||||
.log-underline.log-strikethrough { text-decoration: underline line-through; }
 | 
			
		||||
.log-secret {
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
}
 | 
			
		||||
.log-secret-redacted {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  width: 1px;
 | 
			
		||||
  font-size: 1px;
 | 
			
		||||
}
 | 
			
		||||
.log-fg-black { color: rgb(128,128,128); }
 | 
			
		||||
.log-fg-red { color: rgb(255,0,0); }
 | 
			
		||||
.log-fg-green { color: rgb(0,255,0); }
 | 
			
		||||
.log-fg-yellow { color: rgb(255,255,0); }
 | 
			
		||||
.log-fg-blue { color: rgb(0,0,255); }
 | 
			
		||||
.log-fg-magenta { color: rgb(255,0,255); }
 | 
			
		||||
.log-fg-cyan { color: rgb(0,255,255); }
 | 
			
		||||
.log-fg-white { color: rgb(187,187,187); }
 | 
			
		||||
.log-bg-black { background-color: rgb(0,0,0); }
 | 
			
		||||
.log-bg-red { background-color: rgb(255,0,0); }
 | 
			
		||||
.log-bg-green { background-color: rgb(0,255,0); }
 | 
			
		||||
.log-bg-yellow { background-color: rgb(255,255,0); }
 | 
			
		||||
.log-bg-blue { background-color: rgb(0,0,255); }
 | 
			
		||||
.log-bg-magenta { background-color: rgb(255,0,255); }
 | 
			
		||||
.log-bg-cyan { background-color: rgb(0,255,255); }
 | 
			
		||||
.log-bg-white { background-color: rgb(255,255,255); }
 | 
			
		||||
 | 
			
		||||
ul.browser-default {
 | 
			
		||||
  padding-left: 30px;
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
  margin-bottom: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ul.browser-default li {
 | 
			
		||||
  list-style-type: initial;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .step.done::before, ul.stepper.horizontal .step.active .step-title::before, ul.stepper.horizontal .step.done .step-title::before {
 | 
			
		||||
  background-color: #3f51b5 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.select-action {
 | 
			
		||||
  width: auto !important;
 | 
			
		||||
  height: auto !important;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal {
 | 
			
		||||
  width: 95%;
 | 
			
		||||
  max-height: 90%;
 | 
			
		||||
  height: 85% !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-footer {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  min-height: 50px;
 | 
			
		||||
  padding-top: 0;
 | 
			
		||||
  color: grey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-footer a {
 | 
			
		||||
  color: #afafaf;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 992px) {
 | 
			
		||||
  .page-footer .left, .page-footer .right {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,202 +0,0 @@
 | 
			
		||||
 | 
			
		||||
                                 Apache License
 | 
			
		||||
                           Version 2.0, January 2004
 | 
			
		||||
                        http://www.apache.org/licenses/
 | 
			
		||||
 | 
			
		||||
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 | 
			
		||||
 | 
			
		||||
   1. Definitions.
 | 
			
		||||
 | 
			
		||||
      "License" shall mean the terms and conditions for use, reproduction,
 | 
			
		||||
      and distribution as defined by Sections 1 through 9 of this document.
 | 
			
		||||
 | 
			
		||||
      "Licensor" shall mean the copyright owner or entity authorized by
 | 
			
		||||
      the copyright owner that is granting the License.
 | 
			
		||||
 | 
			
		||||
      "Legal Entity" shall mean the union of the acting entity and all
 | 
			
		||||
      other entities that control, are controlled by, or are under common
 | 
			
		||||
      control with that entity. For the purposes of this definition,
 | 
			
		||||
      "control" means (i) the power, direct or indirect, to cause the
 | 
			
		||||
      direction or management of such entity, whether by contract or
 | 
			
		||||
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
 | 
			
		||||
      outstanding shares, or (iii) beneficial ownership of such entity.
 | 
			
		||||
 | 
			
		||||
      "You" (or "Your") shall mean an individual or Legal Entity
 | 
			
		||||
      exercising permissions granted by this License.
 | 
			
		||||
 | 
			
		||||
      "Source" form shall mean the preferred form for making modifications,
 | 
			
		||||
      including but not limited to software source code, documentation
 | 
			
		||||
      source, and configuration files.
 | 
			
		||||
 | 
			
		||||
      "Object" form shall mean any form resulting from mechanical
 | 
			
		||||
      transformation or translation of a Source form, including but
 | 
			
		||||
      not limited to compiled object code, generated documentation,
 | 
			
		||||
      and conversions to other media types.
 | 
			
		||||
 | 
			
		||||
      "Work" shall mean the work of authorship, whether in Source or
 | 
			
		||||
      Object form, made available under the License, as indicated by a
 | 
			
		||||
      copyright notice that is included in or attached to the work
 | 
			
		||||
      (an example is provided in the Appendix below).
 | 
			
		||||
 | 
			
		||||
      "Derivative Works" shall mean any work, whether in Source or Object
 | 
			
		||||
      form, that is based on (or derived from) the Work and for which the
 | 
			
		||||
      editorial revisions, annotations, elaborations, or other modifications
 | 
			
		||||
      represent, as a whole, an original work of authorship. For the purposes
 | 
			
		||||
      of this License, Derivative Works shall not include works that remain
 | 
			
		||||
      separable from, or merely link (or bind by name) to the interfaces of,
 | 
			
		||||
      the Work and Derivative Works thereof.
 | 
			
		||||
 | 
			
		||||
      "Contribution" shall mean any work of authorship, including
 | 
			
		||||
      the original version of the Work and any modifications or additions
 | 
			
		||||
      to that Work or Derivative Works thereof, that is intentionally
 | 
			
		||||
      submitted to Licensor for inclusion in the Work by the copyright owner
 | 
			
		||||
      or by an individual or Legal Entity authorized to submit on behalf of
 | 
			
		||||
      the copyright owner. For the purposes of this definition, "submitted"
 | 
			
		||||
      means any form of electronic, verbal, or written communication sent
 | 
			
		||||
      to the Licensor or its representatives, including but not limited to
 | 
			
		||||
      communication on electronic mailing lists, source code control systems,
 | 
			
		||||
      and issue tracking systems that are managed by, or on behalf of, the
 | 
			
		||||
      Licensor for the purpose of discussing and improving the Work, but
 | 
			
		||||
      excluding communication that is conspicuously marked or otherwise
 | 
			
		||||
      designated in writing by the copyright owner as "Not a Contribution."
 | 
			
		||||
 | 
			
		||||
      "Contributor" shall mean Licensor and any individual or Legal Entity
 | 
			
		||||
      on behalf of whom a Contribution has been received by Licensor and
 | 
			
		||||
      subsequently incorporated within the Work.
 | 
			
		||||
 | 
			
		||||
   2. Grant of Copyright License. Subject to the terms and conditions of
 | 
			
		||||
      this License, each Contributor hereby grants to You a perpetual,
 | 
			
		||||
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 | 
			
		||||
      copyright license to reproduce, prepare Derivative Works of,
 | 
			
		||||
      publicly display, publicly perform, sublicense, and distribute the
 | 
			
		||||
      Work and such Derivative Works in Source or Object form.
 | 
			
		||||
 | 
			
		||||
   3. Grant of Patent License. Subject to the terms and conditions of
 | 
			
		||||
      this License, each Contributor hereby grants to You a perpetual,
 | 
			
		||||
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 | 
			
		||||
      (except as stated in this section) patent license to make, have made,
 | 
			
		||||
      use, offer to sell, sell, import, and otherwise transfer the Work,
 | 
			
		||||
      where such license applies only to those patent claims licensable
 | 
			
		||||
      by such Contributor that are necessarily infringed by their
 | 
			
		||||
      Contribution(s) alone or by combination of their Contribution(s)
 | 
			
		||||
      with the Work to which such Contribution(s) was submitted. If You
 | 
			
		||||
      institute patent litigation against any entity (including a
 | 
			
		||||
      cross-claim or counterclaim in a lawsuit) alleging that the Work
 | 
			
		||||
      or a Contribution incorporated within the Work constitutes direct
 | 
			
		||||
      or contributory patent infringement, then any patent licenses
 | 
			
		||||
      granted to You under this License for that Work shall terminate
 | 
			
		||||
      as of the date such litigation is filed.
 | 
			
		||||
 | 
			
		||||
   4. Redistribution. You may reproduce and distribute copies of the
 | 
			
		||||
      Work or Derivative Works thereof in any medium, with or without
 | 
			
		||||
      modifications, and in Source or Object form, provided that You
 | 
			
		||||
      meet the following conditions:
 | 
			
		||||
 | 
			
		||||
      (a) You must give any other recipients of the Work or
 | 
			
		||||
          Derivative Works a copy of this License; and
 | 
			
		||||
 | 
			
		||||
      (b) You must cause any modified files to carry prominent notices
 | 
			
		||||
          stating that You changed the files; and
 | 
			
		||||
 | 
			
		||||
      (c) You must retain, in the Source form of any Derivative Works
 | 
			
		||||
          that You distribute, all copyright, patent, trademark, and
 | 
			
		||||
          attribution notices from the Source form of the Work,
 | 
			
		||||
          excluding those notices that do not pertain to any part of
 | 
			
		||||
          the Derivative Works; and
 | 
			
		||||
 | 
			
		||||
      (d) If the Work includes a "NOTICE" text file as part of its
 | 
			
		||||
          distribution, then any Derivative Works that You distribute must
 | 
			
		||||
          include a readable copy of the attribution notices contained
 | 
			
		||||
          within such NOTICE file, excluding those notices that do not
 | 
			
		||||
          pertain to any part of the Derivative Works, in at least one
 | 
			
		||||
          of the following places: within a NOTICE text file distributed
 | 
			
		||||
          as part of the Derivative Works; within the Source form or
 | 
			
		||||
          documentation, if provided along with the Derivative Works; or,
 | 
			
		||||
          within a display generated by the Derivative Works, if and
 | 
			
		||||
          wherever such third-party notices normally appear. The contents
 | 
			
		||||
          of the NOTICE file are for informational purposes only and
 | 
			
		||||
          do not modify the License. You may add Your own attribution
 | 
			
		||||
          notices within Derivative Works that You distribute, alongside
 | 
			
		||||
          or as an addendum to the NOTICE text from the Work, provided
 | 
			
		||||
          that such additional attribution notices cannot be construed
 | 
			
		||||
          as modifying the License.
 | 
			
		||||
 | 
			
		||||
      You may add Your own copyright statement to Your modifications and
 | 
			
		||||
      may provide additional or different license terms and conditions
 | 
			
		||||
      for use, reproduction, or distribution of Your modifications, or
 | 
			
		||||
      for any such Derivative Works as a whole, provided Your use,
 | 
			
		||||
      reproduction, and distribution of the Work otherwise complies with
 | 
			
		||||
      the conditions stated in this License.
 | 
			
		||||
 | 
			
		||||
   5. Submission of Contributions. Unless You explicitly state otherwise,
 | 
			
		||||
      any Contribution intentionally submitted for inclusion in the Work
 | 
			
		||||
      by You to the Licensor shall be under the terms and conditions of
 | 
			
		||||
      this License, without any additional terms or conditions.
 | 
			
		||||
      Notwithstanding the above, nothing herein shall supersede or modify
 | 
			
		||||
      the terms of any separate license agreement you may have executed
 | 
			
		||||
      with Licensor regarding such Contributions.
 | 
			
		||||
 | 
			
		||||
   6. Trademarks. This License does not grant permission to use the trade
 | 
			
		||||
      names, trademarks, service marks, or product names of the Licensor,
 | 
			
		||||
      except as required for reasonable and customary use in describing the
 | 
			
		||||
      origin of the Work and reproducing the content of the NOTICE file.
 | 
			
		||||
 | 
			
		||||
   7. Disclaimer of Warranty. Unless required by applicable law or
 | 
			
		||||
      agreed to in writing, Licensor provides the Work (and each
 | 
			
		||||
      Contributor provides its Contributions) on an "AS IS" BASIS,
 | 
			
		||||
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 | 
			
		||||
      implied, including, without limitation, any warranties or conditions
 | 
			
		||||
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
 | 
			
		||||
      PARTICULAR PURPOSE. You are solely responsible for determining the
 | 
			
		||||
      appropriateness of using or redistributing the Work and assume any
 | 
			
		||||
      risks associated with Your exercise of permissions under this License.
 | 
			
		||||
 | 
			
		||||
   8. Limitation of Liability. In no event and under no legal theory,
 | 
			
		||||
      whether in tort (including negligence), contract, or otherwise,
 | 
			
		||||
      unless required by applicable law (such as deliberate and grossly
 | 
			
		||||
      negligent acts) or agreed to in writing, shall any Contributor be
 | 
			
		||||
      liable to You for damages, including any direct, indirect, special,
 | 
			
		||||
      incidental, or consequential damages of any character arising as a
 | 
			
		||||
      result of this License or out of the use or inability to use the
 | 
			
		||||
      Work (including but not limited to damages for loss of goodwill,
 | 
			
		||||
      work stoppage, computer failure or malfunction, or any and all
 | 
			
		||||
      other commercial damages or losses), even if such Contributor
 | 
			
		||||
      has been advised of the possibility of such damages.
 | 
			
		||||
 | 
			
		||||
   9. Accepting Warranty or Additional Liability. While redistributing
 | 
			
		||||
      the Work or Derivative Works thereof, You may choose to offer,
 | 
			
		||||
      and charge a fee for, acceptance of support, warranty, indemnity,
 | 
			
		||||
      or other liability obligations and/or rights consistent with this
 | 
			
		||||
      License. However, in accepting such obligations, You may act only
 | 
			
		||||
      on Your own behalf and on Your sole responsibility, not on behalf
 | 
			
		||||
      of any other Contributor, and only if You agree to indemnify,
 | 
			
		||||
      defend, and hold each Contributor harmless for any liability
 | 
			
		||||
      incurred by, or claims asserted against, such Contributor by reason
 | 
			
		||||
      of your accepting any such warranty or additional liability.
 | 
			
		||||
 | 
			
		||||
   END OF TERMS AND CONDITIONS
 | 
			
		||||
 | 
			
		||||
   APPENDIX: How to apply the Apache License to your work.
 | 
			
		||||
 | 
			
		||||
      To apply the Apache License to your work, attach the following
 | 
			
		||||
      boilerplate notice, with the fields enclosed by brackets "[]"
 | 
			
		||||
      replaced with your own identifying information. (Don't include
 | 
			
		||||
      the brackets!)  The text should be enclosed in the appropriate
 | 
			
		||||
      comment syntax for the file format. We also recommend that a
 | 
			
		||||
      file or class name and description of purpose be included on the
 | 
			
		||||
      same "printed page" as the copyright notice for easier
 | 
			
		||||
      identification within third-party archives.
 | 
			
		||||
 | 
			
		||||
   Copyright [yyyy] [name of copyright owner]
 | 
			
		||||
 | 
			
		||||
   Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
   you may not use this file except in compliance with the License.
 | 
			
		||||
   You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
       http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
   Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
   distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
   See the License for the specific language governing permissions and
 | 
			
		||||
   limitations under the License.
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -1,12 +0,0 @@
 | 
			
		||||
The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts:
 | 
			
		||||
 | 
			
		||||
```html
 | 
			
		||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
 | 
			
		||||
      rel="stylesheet">
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Read more in our full usage guide:
 | 
			
		||||
http://google.github.io/material-design-icons/#icon-font-for-the-web
 | 
			
		||||
 | 
			
		||||
Source:
 | 
			
		||||
https://github.com/google/material-design-icons
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: 'Material Icons';
 | 
			
		||||
  font-style: normal;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
  src: local('Material Icons'),
 | 
			
		||||
       local('MaterialIcons-Regular'),
 | 
			
		||||
       url(MaterialIcons-Regular.woff2) format('woff2'),
 | 
			
		||||
       url(MaterialIcons-Regular.woff) format('woff');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.material-icons {
 | 
			
		||||
  font-family: 'Material Icons';
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
  font-style: normal;
 | 
			
		||||
  font-size: 24px;  /* Preferred icon size */
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
  text-transform: none;
 | 
			
		||||
  letter-spacing: normal;
 | 
			
		||||
  word-wrap: normal;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  direction: ltr;
 | 
			
		||||
 | 
			
		||||
  /* Support for all WebKit browsers. */
 | 
			
		||||
  -webkit-font-smoothing: antialiased;
 | 
			
		||||
  /* Support for Safari and Chrome. */
 | 
			
		||||
  text-rendering: optimizeLegibility;
 | 
			
		||||
 | 
			
		||||
  /* Support for Firefox. */
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
 | 
			
		||||
  /* Support for IE. */
 | 
			
		||||
  font-feature-settings: 'liga';
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 15 KiB  | 
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16
									
								
								esphome/dashboard/static/js/vendor/ace/ace.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								esphome/dashboard/static/js/vendor/ace/ace.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,7 +0,0 @@
 | 
			
		||||
define("ace/theme/dreamweaver", ["require", "exports", "module", "ace/lib/dom"], function (e, t, n) { t.isDark = !1, t.cssClass = "ace-dreamweaver", t.cssText = '.ace-dreamweaver .ace_gutter {background: #e8e8e8;color: #333;}.ace-dreamweaver .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-dreamweaver {background-color: #FFFFFF;color: black;}.ace-dreamweaver .ace_fold {background-color: #757AD8;}.ace-dreamweaver .ace_cursor {color: black;}.ace-dreamweaver .ace_invisible {color: rgb(191, 191, 191);}.ace-dreamweaver .ace_storage,.ace-dreamweaver .ace_keyword {color: blue;}.ace-dreamweaver .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-dreamweaver .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-dreamweaver .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-dreamweaver .ace_invalid {background-color: rgb(153, 0, 0);color: white;}.ace-dreamweaver .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-dreamweaver .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-dreamweaver .ace_support.ace_type,.ace-dreamweaver .ace_support.ace_class {color: #009;}.ace-dreamweaver .ace_support.ace_php_tag {color: #f00;}.ace-dreamweaver .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-dreamweaver .ace_string {color: #00F;}.ace-dreamweaver .ace_comment {color: rgb(76, 136, 107);}.ace-dreamweaver .ace_comment.ace_doc {color: rgb(0, 102, 255);}.ace-dreamweaver .ace_comment.ace_doc.ace_tag {color: rgb(128, 159, 191);}.ace-dreamweaver .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-dreamweaver .ace_variable {color: #06F}.ace-dreamweaver .ace_xml-pe {color: rgb(104, 104, 91);}.ace-dreamweaver .ace_entity.ace_name.ace_function {color: #00F;}.ace-dreamweaver .ace_heading {color: rgb(12, 7, 255);}.ace-dreamweaver .ace_list {color:rgb(185, 6, 144);}.ace-dreamweaver .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-dreamweaver .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-dreamweaver .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-dreamweaver .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-dreamweaver .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-dreamweaver .ace_gutter-active-line {background-color : #DCDCDC;}.ace-dreamweaver .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-dreamweaver .ace_meta.ace_tag {color:#009;}.ace-dreamweaver .ace_meta.ace_tag.ace_anchor {color:#060;}.ace-dreamweaver .ace_meta.ace_tag.ace_form {color:#F90;}.ace-dreamweaver .ace_meta.ace_tag.ace_image {color:#909;}.ace-dreamweaver .ace_meta.ace_tag.ace_script {color:#900;}.ace-dreamweaver .ace_meta.ace_tag.ace_style {color:#909;}.ace-dreamweaver .ace_meta.ace_tag.ace_table {color:#099;}.ace-dreamweaver .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-dreamweaver .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}'; var r = e("../lib/dom"); r.importCssString(t.cssText, t.cssClass) }); (function () {
 | 
			
		||||
  window.require(["ace/theme/dreamweaver"], function (m) {
 | 
			
		||||
    if (typeof module == "object" && typeof exports == "object" && module) {
 | 
			
		||||
      module.exports = m;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
})();
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,678 +0,0 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>Dashboard - ESPHome</title>
 | 
			
		||||
 | 
			
		||||
  <link rel="shortcut icon" href="{{ get_static_file_url('images/favicon.ico') }}">
 | 
			
		||||
 | 
			
		||||
  <link rel="stylesheet" href="{{ get_static_file_url('fonts/material-icons/material-icons.css') }}">
 | 
			
		||||
  <link rel="stylesheet" href="{{ get_static_file_url('css/vendor/materialize/materialize.min.css') }}">
 | 
			
		||||
  <link rel="stylesheet" href="{{ get_static_file_url('css/vendor/materialize-stepper/materialize-stepper.min.css') }}">
 | 
			
		||||
  <link rel="stylesheet" href="{{ get_static_file_url('css/esphome.css') }}">
 | 
			
		||||
 | 
			
		||||
  <script src="{{ get_static_file_url('js/vendor/jquery/jquery.min.js') }}"></script>
 | 
			
		||||
  <script src="{{ get_static_file_url('js/vendor/jquery-ui/jquery-ui.min.js') }}"></script>
 | 
			
		||||
  <script src="{{ get_static_file_url('js/vendor/jquery-validate/jquery.validate.min.js') }}"></script>
 | 
			
		||||
  <script src="{{ get_static_file_url('js/vendor/materialize/materialize.min.js') }}"></script>
 | 
			
		||||
  <script src="{{ get_static_file_url('js/vendor/materialize-stepper/materialize-stepper.min.js') }}"></script>
 | 
			
		||||
 | 
			
		||||
  {% if streamer_mode %}
 | 
			
		||||
  <style>
 | 
			
		||||
    .log-secret {
 | 
			
		||||
      visibility: hidden !important;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
  {% end %}
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
  <div class="navbar-fixed">
 | 
			
		||||
    <nav class="grey lighten-2">
 | 
			
		||||
      <div class="nav-wrapper">
 | 
			
		||||
        <a href="#" class="black-text logo-wrapper">
 | 
			
		||||
          <img src="https://esphome.io/_static/logo-text.svg" alt="ESPHome Logo" class="logo">
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <ul class="nav-icons right">
 | 
			
		||||
          <li>
 | 
			
		||||
            <a class="dropdown-trigger" href="#!" data-target="nav-dropdown"><i class="material-icons">more_vert</i></a>
 | 
			
		||||
          </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
 | 
			
		||||
        <div class="serial-port-select right" id="js-serial-port-select">
 | 
			
		||||
          <select></select>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <ul id="nav-dropdown" class="select-action dropdown-content">
 | 
			
		||||
        <li><a data-action="update-all" data-filename="{{ escape(config_dir) }}">Update All</a></li>
 | 
			
		||||
        <li><a data-action="edit" data-filename="secrets.yaml">Secrets Editor</a></li>
 | 
			
		||||
        <li class="divider"></li>
 | 
			
		||||
        {% if login_enabled %}
 | 
			
		||||
        <li><a href="{{ relative_url }}logout">Logout</a></li>
 | 
			
		||||
        {% end %}
 | 
			
		||||
      </ul>
 | 
			
		||||
    </nav>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <main>
 | 
			
		||||
    <div id="nodes" class="container">
 | 
			
		||||
 | 
			
		||||
      <div id="grid" class="grid">
 | 
			
		||||
 | 
			
		||||
        {% for i, entry in enumerate(entries) %}
 | 
			
		||||
        <div class="card status-unknown" data-node-name="{{ entry.name }}" data-filename="{{ entry.filename }}">
 | 
			
		||||
          <div class="card-content black-text">
 | 
			
		||||
            <span class="card-title">
 | 
			
		||||
              <span class="node-name">{{ escape(entry.name) }}</span>
 | 
			
		||||
 | 
			
		||||
              <i class="material-icons right dropdown-trigger" data-target="dropdown-{{ i }}">more_vert</i>
 | 
			
		||||
 | 
			
		||||
              {% if 'web_server' in entry.loaded_integrations %}
 | 
			
		||||
              <a href="http://{{ escape(entry.address) }}" target="_blank"><i
 | 
			
		||||
                  class="material-icons node-webserver right tooltipped" data-position="left"
 | 
			
		||||
                  data-tooltip="Open Node Web Server Interface">launch</i></a>
 | 
			
		||||
              {% end %}
 | 
			
		||||
 | 
			
		||||
              {% if entry.update_available %}
 | 
			
		||||
              <i class="material-icons node-update-avaliable right tooltipped" data-position="left"
 | 
			
		||||
                data-tooltip="Update Available: {{ entry.update_old }} ➡️{{ entry.update_new }}">system_update</i>
 | 
			
		||||
              {% end %}
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
            <div class="node-config-path">
 | 
			
		||||
              Filename:
 | 
			
		||||
              <code class="inlinecode tooltipped" data-position="bottom"
 | 
			
		||||
                data-tooltip="Full Path: <code class="inlinecode">{{ escape(entry.path) }}</code>">
 | 
			
		||||
                {{ escape(entry.filename) }}
 | 
			
		||||
              </code>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {% if entry.comment %}
 | 
			
		||||
            <div class="node-card-comment">
 | 
			
		||||
              {{ escape(entry.comment) }}
 | 
			
		||||
            </div>
 | 
			
		||||
            {% end %}
 | 
			
		||||
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="card-action">
 | 
			
		||||
            <a data-action="edit" data-filename="{{ entry.filename }}">Edit</a>
 | 
			
		||||
            <a data-action="validate" data-filename="{{ entry.filename }}">Validate</a>
 | 
			
		||||
            <a data-action="upload" data-filename="{{ entry.filename }}">Upload</a>
 | 
			
		||||
            <a data-action="logs" data-filename="{{ entry.filename }}">Logs</a>
 | 
			
		||||
          </div>
 | 
			
		||||
          <ul id="dropdown-{{ i }}" class="select-action dropdown-content card-dropdown-action node-dropdown">
 | 
			
		||||
            <li><a data-action="clean-mqtt" data-filename="{{ entry.filename }}">Clean MQTT</a></li>
 | 
			
		||||
            <li><a data-action="clean" data-filename="{{ entry.filename }}">Clean Build Files</a></li>
 | 
			
		||||
            <li><a data-action="compile" data-filename="{{ entry.filename }}">Compile</a></li>
 | 
			
		||||
            <li class="divider"></li>
 | 
			
		||||
            <li><a data-action="delete" data-filename="{{ entry.filename }}">Delete</a></li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% end %}
 | 
			
		||||
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {% if len(entries) == 0 %}
 | 
			
		||||
    <div class="center">
 | 
			
		||||
      <h5>Welcome to ESPHome</h5>
 | 
			
		||||
      <p>It looks like you don't yet have any Nodes configured.</p>
 | 
			
		||||
      <p>Click on the pulsating button at the bottom right of the page to add a Node.</p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
      document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
        const addNodeButton = document.querySelector("#js-wizard");
 | 
			
		||||
        addNodeButton.classList.add("pulse");
 | 
			
		||||
      });
 | 
			
		||||
    </script>
 | 
			
		||||
    {% end %}
 | 
			
		||||
 | 
			
		||||
    <!-- Config Editor Modal -->
 | 
			
		||||
    <div id="js-editor-modal" class="modal modal-fixed-footer no-autoinit">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <h4>Editing: <code id="js-node-filename" class="inlinecode"></code></h4>
 | 
			
		||||
 | 
			
		||||
        <div id="js-loading-indicator">
 | 
			
		||||
          <div class="preloader-wrapper big active">
 | 
			
		||||
            <div class="spinner-layer spinner-blue-only">
 | 
			
		||||
              <div class="circle-clipper left">
 | 
			
		||||
                <div class="circle"></div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="gap-patch">
 | 
			
		||||
                <div class="circle"></div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="circle-clipper right">
 | 
			
		||||
                <div class="circle"></div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div id="js-editor-area" class="editor"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a class="waves-effect waves-green btn-flat" data-action="save">Save</a>
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="upload">Upload</a>
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="close">Close</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Upload Modal -->
 | 
			
		||||
    <div id="js-upload-modal" class="modal modal-fixed-footer no-autoinit">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <h4>Compiling & Uploading: <code id="js-node-filename" class="inlinecode"></code></h4>
 | 
			
		||||
        <pre id="js-log-area" class="log"></pre>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a href="https://esphome.io/guides/faq.html#i-can-t-get-flashing-over-usb-to-work" target="_blank"
 | 
			
		||||
          rel="noreferrer">
 | 
			
		||||
          <i class="material-icons">help_outline</i>
 | 
			
		||||
        </a>
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="edit">Edit</a>
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="stop-logs">Stop</a>
 | 
			
		||||
        <div class="btn-flat"><i class="material-icons dropdown-trigger"
 | 
			
		||||
            data-target="dropdown-upload-actions">more_vert</i>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <ul id="dropdown-upload-actions" class="select-action dropdown-content card-dropdown-action">
 | 
			
		||||
        <li>
 | 
			
		||||
          <a class="modal-close waves-effect waves-green btn-flat disabled" data-action="download-binary">
 | 
			
		||||
            Download Binary
 | 
			
		||||
          </a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
          <a class="waves-effect waves-green btn-flat disabled action-upload" data-action="upload">Retry Upload</a>
 | 
			
		||||
        </li>
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Update All Modal -->
 | 
			
		||||
    <div id="js-update-all-modal" class="modal modal-fixed-footer no-autoinit">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <h4>Update All <code id="js-node-filename" class="inlinecode"></code></h4>
 | 
			
		||||
        <pre id="js-log-area" class="log"></pre>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="stop-logs">Stop</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Validate Modal -->
 | 
			
		||||
    <div id="js-validate-modal" class="modal modal-fixed-footer no-autoinit">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <h4>Validate: <code id="js-node-filename" class="inlinecode"></code></h4>
 | 
			
		||||
        <pre id="js-log-area" class="log"></pre>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="edit">Edit</a>
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="upload">Upload</a>
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="stop-logs">Stop</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Logs Modal -->
 | 
			
		||||
    <div id="js-logs-modal" class="modal modal-fixed-footer no-autoinit">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <!-- TODO: Change `node-filename` to `node-name` -->
 | 
			
		||||
        <h4>Showing Logs For: <code id="js-node-filename" class="inlinecode"></code></h4>
 | 
			
		||||
        <pre id="js-log-area" class="log"></pre>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="stop-logs">Close</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Compile Modal -->
 | 
			
		||||
    <div id="js-compile-modal" class="modal modal-fixed-footer no-autoinit">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <h4>Compiling: <code id="js-node-filename" class="inlinecode"></code></h4>
 | 
			
		||||
        <pre id="js-log-area" class="log"></pre>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a href="https://esphome.io/guides/faq.html#i-can-t-get-flashing-over-usb-to-work" target="_blank"
 | 
			
		||||
          rel="noreferrer">
 | 
			
		||||
          <i class="material-icons">help_outline</i>
 | 
			
		||||
        </a>
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat disabled" data-action="download-binary">
 | 
			
		||||
          Download Binary
 | 
			
		||||
        </a>
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="stop-logs">Stop</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Clean MQTT Modal -->
 | 
			
		||||
    <div id="js-clean-mqtt-modal" class="modal modal-fixed-footer no-autoinit">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <div>
 | 
			
		||||
          <!-- TODO: Change `node-filename` to `node-name` -->
 | 
			
		||||
          <h4>Clean MQTT Discovery For: <code id="js-node-filename" class="inlinecode"></code></h4>
 | 
			
		||||
          <pre id="js-log-area" class="log"></pre>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="stop-logs">Close</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Clean Build Files Modal -->
 | 
			
		||||
    <div id="js-clean-modal" class="modal modal-fixed-footer no-autoinit">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <div>
 | 
			
		||||
          <!-- TODO: Change `node-filename` to `node-name` -->
 | 
			
		||||
          <h4>Clean Build Files For: <code id="js-node-filename" class="inlinecode"></code></h4>
 | 
			
		||||
          <pre id="js-log-area" class="log"></pre>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" data-action="stop-logs">Stop</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Add New Node / Launch Wizard Button -->
 | 
			
		||||
    <div class="fixed-action-btn">
 | 
			
		||||
      <a class="btn-floating btn-large waves-effect waves-light green js-wizard" id="js-wizard" data-action="wizard">
 | 
			
		||||
        <i class="large material-icons">add</i>
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Wizard Modal -->
 | 
			
		||||
    <div id="js-wizard-modal" class="modal modal-fixed-footer">
 | 
			
		||||
      <div class="modal-content">
 | 
			
		||||
        <h5>Create New Node</h5>
 | 
			
		||||
        <form action="./wizard.html" method="POST">
 | 
			
		||||
          <ul class="stepper">
 | 
			
		||||
            {% if len(entries) == 0 %}
 | 
			
		||||
            <!-- Step 0 (First Run - No Nodes) - Welcome -->
 | 
			
		||||
            <li class="step active">
 | 
			
		||||
              <div class="step-title waves-effect">Welcome</div>
 | 
			
		||||
              <div class="step-content">
 | 
			
		||||
                <p>
 | 
			
		||||
                  Welcome to the ESPHome node setup wizard! As there are no nodes setup you will be guided through
 | 
			
		||||
                  setting up your first ESP8266 or ESP32-powered device using ESPHome.
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                  <a href="https://www.espressif.com/en/products/hardware/esp8266ex/overview"
 | 
			
		||||
                    target="_blank">ESP8266s</a> and
 | 
			
		||||
                  their successors (the <a href="https://www.espressif.com/en/products/hardware/esp32/overview"
 | 
			
		||||
                    target="_blank">ESP32s</a>)
 | 
			
		||||
                  are great low-cost microcontrollers that can communicate with the outside world using WiFi.
 | 
			
		||||
                  They're found in many devices such as the popular Sonoff/iTead, but also exist as development boards
 | 
			
		||||
                  such as the <a href="https://esphome.io/devices/nodemcu_esp8266.html" rel="noreferrer"
 | 
			
		||||
                    target="_blank">NodeMCU</a>.
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                  ESPHome, the tool you're using here, creates custom firmwares for these devices using YAML
 | 
			
		||||
                  configuration files (similar to the ones you might be used to with <a href="https://home-assistant.io"
 | 
			
		||||
                    target="_blank">Home Assistant</a>).
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                  This wizard will create a basic YAML configuration file for your "node" (the microcontroller) and
 | 
			
		||||
                  later, you will be able to customize this file with some of ESPHome's many avaliable integrations.
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                  When you are ready click "begin" to move onto the next step!
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <div class="step-actions">
 | 
			
		||||
                  <button class="waves-effect waves-dark green btn next-step">Begin</button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </li>
 | 
			
		||||
            {% end %}
 | 
			
		||||
 | 
			
		||||
            <!-- Step 1 - Node Name -->
 | 
			
		||||
            {% if len(entries) == 0 %}
 | 
			
		||||
            <li class="step">
 | 
			
		||||
              {% end %}
 | 
			
		||||
              {% if len(entries) >= 1 %}
 | 
			
		||||
            <li class="step active">
 | 
			
		||||
              {% end %}
 | 
			
		||||
              <div class="step-title waves-effect">Node Name</div>
 | 
			
		||||
              <div class="step-content">
 | 
			
		||||
 | 
			
		||||
                {% if len(entries) == 0 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  Firstly, please decide on a name for the node. Choose this name wisely, it should be unique among all
 | 
			
		||||
                  of your ESPHome nodes.
 | 
			
		||||
                </p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                {% if len(entries) >= 1 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  Select a unique name for the node.
 | 
			
		||||
                </p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                  Names must be all <strong>lowercase</strong> and <strong>must not contain any spaces</strong>!
 | 
			
		||||
                  Characters that are allowed are: <code class="inlinecode">a-z</code>,
 | 
			
		||||
                  <code class="inlinecode">0-9</code> and <code class="inlinecode">-</code>.
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <div class="input-field col s12">
 | 
			
		||||
                  <input id="node_name" class="validate" type="text" name="name" data-rule-nospaces="true"
 | 
			
		||||
                    data-rule-lowercase="true" data-rule-nounderscores="true" required>
 | 
			
		||||
                  <label for="node_name">Node Name</label>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="step-actions">
 | 
			
		||||
                  <button class="waves-effect waves-dark green btn next-step">Next</button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </li>
 | 
			
		||||
 | 
			
		||||
            <!-- Step 2 - Device Type -->
 | 
			
		||||
            <li class="step">
 | 
			
		||||
              <div class="step-title waves-effect">Device Type</div>
 | 
			
		||||
              <div class="step-content">
 | 
			
		||||
 | 
			
		||||
                {% if len(entries) == 0 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  Now that you have named your node, please select what type of microcontroller you are using so that
 | 
			
		||||
                  the firmware for your node can be compiled correctly.
 | 
			
		||||
                </p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                {% if len(entries) >= 1 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  Select the type of microcontroller that you are using for the node.
 | 
			
		||||
                </p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                  <em>If unsure you can also select a similar board or choose the "Generic" option.</em>
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <div class="input-field col s12">
 | 
			
		||||
                  <select class="browser-default" name="board" required>
 | 
			
		||||
                    <optgroup label="ESP8266">
 | 
			
		||||
                      <option value="esp01_1m">Generic ESP8266 (for example Sonoff)</option>
 | 
			
		||||
                      <option value="nodemcuv2">NodeMCU</option>
 | 
			
		||||
                      <option value="d1_mini">Wemos D1 and Wemos D1 mini</option>
 | 
			
		||||
                      <option value="d1_mini_lite">Wemos D1 mini Lite</option>
 | 
			
		||||
                      <option value="d1_mini_pro">Wemos D1 mini Pro</option>
 | 
			
		||||
                      <option value="huzzah">Adafruit HUZZAH ESP8266</option>
 | 
			
		||||
                      <option value="oak">DigiStump Oak</option>
 | 
			
		||||
                      <option value="thing">Sparkfun ESP8266 Thing</option>
 | 
			
		||||
                      <option value="thingdev">Sparkfun ESP8266 Thing - Dev Board</option>
 | 
			
		||||
                    </optgroup>
 | 
			
		||||
                    <optgroup label="ESP32">
 | 
			
		||||
                      <option value="esp-wrover-kit">Generic ESP32 (WROVER Module)</option>
 | 
			
		||||
                      <option value="nodemcu-32s">NodeMCU-32S</option>
 | 
			
		||||
                      <option value="lolin_d32">Wemos Lolin D32</option>
 | 
			
		||||
                      <option value="lolin_d32_pro">Wemos Lolin D32 Pro</option>
 | 
			
		||||
                      <option value="featheresp32">Adafruit ESP32 Feather</option>
 | 
			
		||||
                      <option value="m5stack-core-esp32">M5Stack Core ESP32</option>
 | 
			
		||||
                    </optgroup>
 | 
			
		||||
                    <optgroup label="Other ESP8266s">
 | 
			
		||||
                      <option value="gen4iod">4D Systems gen4 IoD Range</option>
 | 
			
		||||
                      <option value="wifi_slot">Amperka WiFi Slot</option>
 | 
			
		||||
                      <option value="espduino">Doit ESPDuino</option>
 | 
			
		||||
                      <option value="espectro">DycodeX ESPectro Core</option>
 | 
			
		||||
                      <option value="espino">ESPino</option>
 | 
			
		||||
                      <option value="esp_wroom_02">Espressif ESP-WROOM-02 module</option>
 | 
			
		||||
                      <option value="esp12e">Espressif ESP-12E module</option>
 | 
			
		||||
                      <option value="esp01">Espressif ESP-01 512k module</option>
 | 
			
		||||
                      <option value="esp07">Espressif ESP-07 module</option>
 | 
			
		||||
                      <option value="esp8285">Generic ESP8285 module</option>
 | 
			
		||||
                      <option value="espresso_lite_v1">ESPert ESPresso Lite 1.0</option>
 | 
			
		||||
                      <option value="espresso_lite_v2">ESPert ESPresso Lite 2.0</option>
 | 
			
		||||
                      <option value="phoenix_v1">ESPert Phoenix 1.0</option>
 | 
			
		||||
                      <option value="wifinfo">WiFInfo</option>
 | 
			
		||||
                      <option value="heltec_wifi_kit_8">Heltec WiFi kit 8</option>
 | 
			
		||||
                      <option value="nodemcu">NodeMCU 0.9</option>
 | 
			
		||||
                      <option value="modwifi">Olimex MOD-WIFI</option>
 | 
			
		||||
                      <option value="wio_link">SeedStudio Wio Link</option>
 | 
			
		||||
                      <option value="wio_node">SeedStudio Wio Node</option>
 | 
			
		||||
                      <option value="sparkfunBlynk">Sparkfun Blynk Board</option>
 | 
			
		||||
                      <option value="esp210">SweetPea ESP-210</option>
 | 
			
		||||
                      <option value="espinotee">ThaiEasyElec ESPino</option>
 | 
			
		||||
                      <option value="d1">Wemos D1 Revision 1</option>
 | 
			
		||||
                      <option value="wifiduino">WiFiDuino</option>
 | 
			
		||||
                      <option value="xinabox_cw01">XinaBox CW01</option>
 | 
			
		||||
                    </optgroup>
 | 
			
		||||
                    <optgroup label="Other ESP32s">
 | 
			
		||||
                      <option value="lolin32">Wemos Lolin 32</option>
 | 
			
		||||
                      <option value="m5stack-fire">M5Stack FIRE</option>
 | 
			
		||||
                      <option value="wemosbat">Wemos WiFi & Bluetooth Battery</option>
 | 
			
		||||
                      <option value="node32s">Node32s</option>
 | 
			
		||||
                      <option value="alksesp32">ALKS ESP32</option>
 | 
			
		||||
                      <option value="bpi-bit">BPI-Bit</option>
 | 
			
		||||
                      <option value="d-duino-32">D-duino-32</option>
 | 
			
		||||
                      <option value="esp32-devkitlipo">OLIMEX ESP32-DevKit-LiPo</option>
 | 
			
		||||
                      <option value="esp32-evb">OLIMEX ESP32-EVB</option>
 | 
			
		||||
                      <option value="esp32-gateway">OLIMEX ESP32-GATEWAY</option>
 | 
			
		||||
                      <option value="esp32-poe-iso">OLIMEX ESP32-PoE-ISO</option>
 | 
			
		||||
                      <option value="esp32-poe">OLIMEX ESP32-PoE</option>
 | 
			
		||||
                      <option value="esp32-pro">OLIMEX ESP32-PRO</option>
 | 
			
		||||
                      <option value="esp320">Electronic SweetPeas ESP320</option>
 | 
			
		||||
                      <option value="esp32cam">AI Thinker ESP32-CAM</option>
 | 
			
		||||
                      <option value="esp32dev">Espressif ESP32 Dev Module</option>
 | 
			
		||||
                      <option value="esp32doit-devkit-v1">DOIT ESP32 DEVKIT V1</option>
 | 
			
		||||
                      <option value="esp32thing">SparkFun ESP32 Thing</option>
 | 
			
		||||
                      <option value="esp32vn-iot-uno">ESP32vn IoT Uno</option>
 | 
			
		||||
                      <option value="espea32">April Brother ESPea32</option>
 | 
			
		||||
                      <option value="espectro32">ESPectro32</option>
 | 
			
		||||
                      <option value="espino32">ESPino32</option>
 | 
			
		||||
                      <option value="firebeetle32">FireBeetle-ESP32</option>
 | 
			
		||||
                      <option value="fm-devkit">ESP32 FM DevKit</option>
 | 
			
		||||
                      <option value="frogboard">Frog Board ESP32</option>
 | 
			
		||||
                      <option value="heltec_wifi_kit_32">Heltec WiFi Kit 32</option>
 | 
			
		||||
                      <option value="heltec_wifi_lora_32">Heltec WiFi LoRa 32</option>
 | 
			
		||||
                      <option value="heltec_wifi_lora_32_V2">Heltec WiFi LoRa 32 (V2)</option>
 | 
			
		||||
                      <option value="heltec_wireless_stick">Heltec Wireless Stick</option>
 | 
			
		||||
                      <option value="hornbill32dev">Hornbill ESP32 Dev</option>
 | 
			
		||||
                      <option value="hornbill32minima">Hornbill ESP32 Minima</option>
 | 
			
		||||
                      <option value="intorobot">IntoRobot Fig</option>
 | 
			
		||||
                      <option value="iotaap_magnolia">IoTaaP Magnolia</option>
 | 
			
		||||
                      <option value="iotbusio">oddWires IoT-Bus Io</option>
 | 
			
		||||
                      <option value="iotbusproteus">oddWires IoT-Bus Proteus</option>
 | 
			
		||||
                      <option value="lopy">Pycom LoPy</option>
 | 
			
		||||
                      <option value="lopy4">Pycom LoPy4</option>
 | 
			
		||||
                      <option value="m5stack-grey">M5Stack GREY ESP32</option>
 | 
			
		||||
                      <option value="m5stick-c">M5Stick-C</option>
 | 
			
		||||
                      <option value="magicbit">MagicBit</option>
 | 
			
		||||
                      <option value="mhetesp32devkit">MH ET LIVE ESP32DevKIT</option>
 | 
			
		||||
                      <option value="mhetesp32minikit">MH ET LIVE ESP32MiniKit</option>
 | 
			
		||||
                      <option value="microduino-core-esp32">Microduino Core ESP32</option>
 | 
			
		||||
                      <option value="nano32">MakerAsia Nano32</option>
 | 
			
		||||
                      <option value="nina_w10">u-blox NINA-W10 series</option>
 | 
			
		||||
                      <option value="odroid_esp32">ODROID-GO</option>
 | 
			
		||||
                      <option value="onehorse32dev">Onehorse ESP32 Dev Module</option>
 | 
			
		||||
                      <option value="oroca_edubot">OROCA EduBot</option>
 | 
			
		||||
                      <option value="pico32">ESP32 Pico Kit</option>
 | 
			
		||||
                      <option value="pocket_32">Dongsen Tech Pocket 32</option>
 | 
			
		||||
                      <option value="pycom_gpy">Pycom GPy</option>
 | 
			
		||||
                      <option value="quantum">Noduino Quantum</option>
 | 
			
		||||
                      <option value="sparkfun_lora_gateway_1-channel">SparkFun LoRa Gateway 1-Channel</option>
 | 
			
		||||
                      <option value="tinypico">TinyPICO</option>
 | 
			
		||||
                      <option value="ttgo-lora32-v1">TTGO LoRa32-OLED V1</option>
 | 
			
		||||
                      <option value="ttgo-t-beam">TTGO T-Beam</option>
 | 
			
		||||
                      <option value="ttgo-t-watch">TTGO T-Watch</option>
 | 
			
		||||
                      <option value="ttgo-t1">TTGO T1</option>
 | 
			
		||||
                      <option value="turta_iot_node">Turta IoT Node</option>
 | 
			
		||||
                      <option value="vintlabs-devkit-v1">VintLabs ESP32 Devkit</option>
 | 
			
		||||
                      <option value="wemos_d1_mini32">WeMos D1 MINI ESP32</option>
 | 
			
		||||
                      <option value="wesp32">Silicognition wESP32</option>
 | 
			
		||||
                      <option value="widora-air">Widora AIR</option>
 | 
			
		||||
                      <option value="xinabox_cw02">XinaBox CW02</option>
 | 
			
		||||
                    </optgroup>
 | 
			
		||||
                  </select>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="step-actions">
 | 
			
		||||
                  <button class="waves-effect waves-dark green btn next-step">Next</button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </li>
 | 
			
		||||
 | 
			
		||||
            <!-- Step 3 - WiFi & Updates -->
 | 
			
		||||
            <li class="step">
 | 
			
		||||
              <div class="step-title waves-effect">WiFi & Updates</div>
 | 
			
		||||
              <div class="step-content">
 | 
			
		||||
                {% if len(entries) == 0 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  Finally, please enter the details for the WiFi network you want the node to connect to.
 | 
			
		||||
 | 
			
		||||
                  Please enter an SSID (name of the WiFi network) and the password needed to connect (if the network has
 | 
			
		||||
                  no password then this can be left blank):
 | 
			
		||||
                </p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                {% if len(entries) >= 1 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  Enter the details for the WiFi network the node can connect to:
 | 
			
		||||
                </p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
                <div class="input-field col s12">
 | 
			
		||||
                  <input id="wifi_ssid" class="validate" type="text" name="ssid" required>
 | 
			
		||||
                  <label for="wifi_ssid">WiFi SSID</label>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="input-field col s12">
 | 
			
		||||
                  <input id="wifi_password" name="psk" type="password">
 | 
			
		||||
                  <label for="wifi_password">WiFi Password</label>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {% if len(entries) == 0 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  This wizard will automatically set up an Over-The-Air (OTA) update server on the node so that you only
 | 
			
		||||
                  need
 | 
			
		||||
                  to flash the firmware via USB once.
 | 
			
		||||
 | 
			
		||||
                  You can optionally password protect this OTA update server by setting up a password here:
 | 
			
		||||
                </p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                {% if len(entries) >= 1 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  Setup an optional password for the Over-The-Air (OTA) update server:
 | 
			
		||||
                </p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
                <div class="input-field col s12">
 | 
			
		||||
                  <input id="password" class="validate" name="password" type="password">
 | 
			
		||||
                  <label for="password">OTA Access Password</label>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="step-actions">
 | 
			
		||||
                  <!-- Here goes your actions buttons -->
 | 
			
		||||
                  <button class="waves-effect waves-dark green btn next-step">Next</button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </li>
 | 
			
		||||
 | 
			
		||||
            <!-- Step 3 - Finish -->
 | 
			
		||||
            <li class="step">
 | 
			
		||||
              <div class="step-title waves-effect">Finish</div>
 | 
			
		||||
              <div class="step-content">
 | 
			
		||||
                {% if len(entries) == 0 %}
 | 
			
		||||
                <p>
 | 
			
		||||
                  Hooray! 🎉🎉🎉 You have successfully created your first ESPHome configuration file.
 | 
			
		||||
                  When you click Submit, the wizard will save the configuration file as
 | 
			
		||||
                  <code class="inlinecode">{{ escape(config_dir) }}/<NAME_OF_NODE>.yaml</code>.
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                <h5>Next steps</h5>
 | 
			
		||||
                <ul class="browser-default">
 | 
			
		||||
                  <li>
 | 
			
		||||
                    Flash the firmware. This can be done using the “UPLOAD” option in the dashboard. See
 | 
			
		||||
                    <a href="https://esphome.io/index.html#devices" rel="noreferrer" target="_blank">this</a>
 | 
			
		||||
                    for guides on how to flash different types of devices. For newly plugged in serial
 | 
			
		||||
                    devices to be detected, restart the add-on.
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    With the current configuration, your node will only connect to WiFi. To make it
 | 
			
		||||
                    actually <i>do</i> stuff, follow
 | 
			
		||||
                    <a href="https://esphome.io/guides/getting_started_hassio.html#adding-some-basic-features"
 | 
			
		||||
                      rel="noreferrer">
 | 
			
		||||
                      the rest of the getting started guide
 | 
			
		||||
                    </a>.
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    See the <a href="https://esphome.io/index.html" rel="noreferrer" target="_blank">ESPHome
 | 
			
		||||
                      Documentation</a>
 | 
			
		||||
                    for a list of supported sensors/devices.
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    Join the <a href="https://discord.gg/KhAMKrd" target="_blank">Discord server</a> and
 | 
			
		||||
                    say hi! Discord's the best place to ask if you have issues/ideas.
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                    Star <a href="https://github.com/esphome/esphome" target="_blank">ESPHome</a> on GitHub
 | 
			
		||||
                    if you find this software awesome and report issues using the bug trackers there.
 | 
			
		||||
                  </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                {% if len(entries) >= 1 %}
 | 
			
		||||
                <p>Click submit to finish creating the node!</p>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                <div class="step-actions">
 | 
			
		||||
                  <!-- Here goes your actions buttons -->
 | 
			
		||||
                  <button class="waves-effect waves-dark green btn" type="submit">Submit</button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </li>
 | 
			
		||||
 | 
			
		||||
          </ul>
 | 
			
		||||
        </form>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="modal-footer">
 | 
			
		||||
        <a class="modal-close waves-effect waves-green btn-flat" onclick="wizardStepperInstace.resetStepper();">Exit
 | 
			
		||||
          Wizard</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </main>
 | 
			
		||||
 | 
			
		||||
  <footer class="page-footer grey darken-4">
 | 
			
		||||
    <div class="container">
 | 
			
		||||
      <div class="left">
 | 
			
		||||
        Copyright © 2019-2021 ESPHome | Made with <a href="https://materializecss.com/" target="_blank">Materialize</a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="right">
 | 
			
		||||
        <a href="{{ docs_link }}" target="_blank" rel="noreferrer">v{{ version }}
 | 
			
		||||
          Documentation</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </footer>
 | 
			
		||||
 | 
			
		||||
  <script src="{{ get_static_file_url('js/vendor/ace/ace.js') }}" type="text/javascript" charset="utf-8"></script>
 | 
			
		||||
  <script src="{{ get_static_file_url('js/esphome.js') }}" type="text/javascript"></script>
 | 
			
		||||
 | 
			
		||||
  {% if begin and len(entries) == 1 %}
 | 
			
		||||
  <script>
 | 
			
		||||
    window.history.replaceState({}, document.title, "/");
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
      M.toast({
 | 
			
		||||
        html: '🎉 Congratulations on adding your first Node!',
 | 
			
		||||
        displayLength: 10000
 | 
			
		||||
      })
 | 
			
		||||
    });
 | 
			
		||||
  </script>
 | 
			
		||||
  {% end %}
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1,93 +0,0 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en" id="login-page">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>Login - ESPHome</title>
 | 
			
		||||
 | 
			
		||||
  <link rel="shortcut icon" href="{{ get_static_file_url('images/favicon.ico') }}">
 | 
			
		||||
 | 
			
		||||
  <link rel="stylesheet" href="{{ get_static_file_url('fonts/material-icons/material-icons.css') }}">
 | 
			
		||||
  <link rel="stylesheet" href="{{ get_static_file_url('css/vendor/materialize/materialize.min.css') }}">
 | 
			
		||||
  <link rel="stylesheet" href="{{ get_static_file_url('css/esphome.css') }}">
 | 
			
		||||
 | 
			
		||||
  <script src="{{ get_static_file_url('js/vendor/jquery/jquery.min.js') }}"></script>
 | 
			
		||||
  <script src="{{ get_static_file_url('js/vendor/materialize/materialize.min.js') }}"></script>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
  <div class="valign-wrapper">
 | 
			
		||||
    <div class="valign">
 | 
			
		||||
      <div class="container">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
          <div class="col s12 m8 offset-m2 l8 offset-l2 xl6 offset-xl3">
 | 
			
		||||
            <div class="card" id="login-card">
 | 
			
		||||
              <div class="card-content">
 | 
			
		||||
                <img src="https://esphome.io/_static/logo-text.svg" alt="ESPHome Logo" class="logo">
 | 
			
		||||
                <span class="version-number center">v{{ version }}</span>
 | 
			
		||||
                <span class="card-title black-text center">Dashboard Login</span>
 | 
			
		||||
                <p class="center">
 | 
			
		||||
                  {% if hassio %}
 | 
			
		||||
                  Login by entering your Home Assistant login credentials.
 | 
			
		||||
                  {% else %}
 | 
			
		||||
                  Login by entering your ESPHome login credentials.
 | 
			
		||||
                  {% end %}
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
                {% if error is not None %}
 | 
			
		||||
                <div class="alert alert-error">
 | 
			
		||||
                  <span class="title">Error!</span>
 | 
			
		||||
                  {{ escape(error) }}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <script>
 | 
			
		||||
                  $("#login-card").addClass("card-error");
 | 
			
		||||
                </script>
 | 
			
		||||
                {% end %}
 | 
			
		||||
 | 
			
		||||
                <form action="./login" method="post" id="login-form">
 | 
			
		||||
                  {% if has_username or hassio %}
 | 
			
		||||
                  <div class="row no-bottom-margin">
 | 
			
		||||
                    <div class="input-field col s12">
 | 
			
		||||
                      <i class="material-icons prefix">person</i>
 | 
			
		||||
                      <input name="username" id="username" type="text">
 | 
			
		||||
                      <label for="username">Username</label>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {% end %}
 | 
			
		||||
 | 
			
		||||
                  <div class="row no-bottom-margin">
 | 
			
		||||
                    <div class="input-field col s12">
 | 
			
		||||
                      <i class="material-icons prefix">lock</i>
 | 
			
		||||
                      <input name="password" id="password" type="password">
 | 
			
		||||
                      <label for="password">Password</label>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </form>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div class="card-action center">
 | 
			
		||||
                <input type="submit" class="btn blue-grey darken-2" name="action" form="login-form" value="Login">
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <footer class="center-align">
 | 
			
		||||
        <p>Copyright © 2019-2021 ESPHome.</p>
 | 
			
		||||
        <p>
 | 
			
		||||
          <a href="{{ docs_link }}" target="_blank" rel="noreferrer">ESPHome
 | 
			
		||||
            {{ version }} Documentation</a>
 | 
			
		||||
        </p>
 | 
			
		||||
        <p>
 | 
			
		||||
          Made with <a href="https://materializecss.com/" target="_blank">Materialize.</a>
 | 
			
		||||
        </p>
 | 
			
		||||
      </footer>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										57
									
								
								esphome/final_validate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								esphome/final_validate.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
from abc import ABC, abstractmethod, abstractproperty
 | 
			
		||||
from typing import Dict, Any
 | 
			
		||||
import contextvars
 | 
			
		||||
 | 
			
		||||
from esphome.types import ConfigFragmentType, ID, ConfigPathType
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FinalValidateConfig(ABC):
 | 
			
		||||
    @abstractproperty
 | 
			
		||||
    def data(self) -> Dict[str, Any]:
 | 
			
		||||
        """A dictionary that can be used by post validation functions to store
 | 
			
		||||
        global data during the validation phase. Each component should store its
 | 
			
		||||
        data under a unique key
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def get_path_for_id(self, id: ID) -> ConfigPathType:
 | 
			
		||||
        """Get the config path a given ID has been declared in.
 | 
			
		||||
 | 
			
		||||
        This is the location under the _validated_ config (for example, with cv.ensure_list applied)
 | 
			
		||||
        Raises KeyError if the id was not declared in the configuration.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def get_config_for_path(self, path: ConfigPathType) -> ConfigFragmentType:
 | 
			
		||||
        """Get the config fragment for the given global path.
 | 
			
		||||
 | 
			
		||||
        Raises KeyError if a key in the path does not exist.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FinalValidateConfig.register(dict)
 | 
			
		||||
 | 
			
		||||
# Context variable tracking the full config for some final validation functions.
 | 
			
		||||
full_config: contextvars.ContextVar[FinalValidateConfig] = contextvars.ContextVar(
 | 
			
		||||
    "full_config"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def id_declaration_match_schema(schema):
 | 
			
		||||
    """A final-validation schema function that applies a schema to the outer config fragment of an
 | 
			
		||||
    ID declaration.
 | 
			
		||||
 | 
			
		||||
    This validator must be applied to ID values.
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(schema, cv.Schema):
 | 
			
		||||
        schema = cv.Schema(schema, extra=cv.ALLOW_EXTRA)
 | 
			
		||||
 | 
			
		||||
    def validator(value):
 | 
			
		||||
        fconf = full_config.get()
 | 
			
		||||
        path = fconf.get_path_for_id(value)[:-1]
 | 
			
		||||
        declaration_config = fconf.get_config_for_path(path)
 | 
			
		||||
        with cv.prepend_path([cv.ROOT_CONFIG_PATH] + path):
 | 
			
		||||
            return schema(declaration_config)
 | 
			
		||||
 | 
			
		||||
    return validator
 | 
			
		||||
@@ -12,6 +12,7 @@ from pathlib import Path
 | 
			
		||||
from esphome.const import ESP_PLATFORMS, SOURCE_FILE_EXTENSIONS
 | 
			
		||||
import esphome.core.config
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@@ -81,8 +82,13 @@ class ComponentManifest:
 | 
			
		||||
        return getattr(self.module, "CODEOWNERS", [])
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def validate(self):
 | 
			
		||||
        return getattr(self.module, "validate", None)
 | 
			
		||||
    def final_validate_schema(self) -> Optional[Callable[[ConfigType], None]]:
 | 
			
		||||
        """Components can declare a `FINAL_VALIDATE_SCHEMA` cv.Schema that gets called
 | 
			
		||||
        after the main validation. In that function checks across components can be made.
 | 
			
		||||
 | 
			
		||||
        Note that the function can't mutate the configuration - no changes are saved
 | 
			
		||||
        """
 | 
			
		||||
        return getattr(self.module, "FINAL_VALIDATE_SCHEMA", None)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def source_files(self) -> Dict[Path, SourceFile]:
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,13 @@ from datetime import datetime
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
from typing import Any, Optional, List
 | 
			
		||||
 | 
			
		||||
from esphome import const
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.helpers import write_file_if_changed
 | 
			
		||||
 | 
			
		||||
# pylint: disable=unused-import, wrong-import-order
 | 
			
		||||
from esphome.core import CoreType
 | 
			
		||||
from typing import Any, Optional, List
 | 
			
		||||
from esphome.types import CoreType
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								esphome/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/types.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
"""This helper module tracks commonly used types in the esphome python codebase."""
 | 
			
		||||
from typing import Dict, Union, List
 | 
			
		||||
 | 
			
		||||
from esphome.core import ID, Lambda, EsphomeCore
 | 
			
		||||
 | 
			
		||||
ConfigFragmentType = Union[
 | 
			
		||||
    str,
 | 
			
		||||
    int,
 | 
			
		||||
    float,
 | 
			
		||||
    None,
 | 
			
		||||
    Dict[Union[str, int], "ConfigFragmentType"],
 | 
			
		||||
    List["ConfigFragmentType"],
 | 
			
		||||
    ID,
 | 
			
		||||
    Lambda,
 | 
			
		||||
]
 | 
			
		||||
ConfigType = Dict[str, ConfigFragmentType]
 | 
			
		||||
CoreType = EsphomeCore
 | 
			
		||||
ConfigPathType = Union[str, int]
 | 
			
		||||
@@ -49,17 +49,6 @@ BASE_CONFIG = """esphome:
 | 
			
		||||
  platform: {platform}
 | 
			
		||||
  board: {board}
 | 
			
		||||
 | 
			
		||||
wifi:
 | 
			
		||||
  ssid: "{ssid}"
 | 
			
		||||
  password: "{psk}"
 | 
			
		||||
 | 
			
		||||
  # Enable fallback hotspot (captive portal) in case wifi connection fails
 | 
			
		||||
  ap:
 | 
			
		||||
    ssid: "{fallback_name}"
 | 
			
		||||
    password: "{fallback_psk}"
 | 
			
		||||
 | 
			
		||||
captive_portal:
 | 
			
		||||
 | 
			
		||||
# Enable logging
 | 
			
		||||
logger:
 | 
			
		||||
 | 
			
		||||
@@ -83,12 +72,43 @@ def wizard_file(**kwargs):
 | 
			
		||||
 | 
			
		||||
    config = BASE_CONFIG.format(**kwargs)
 | 
			
		||||
 | 
			
		||||
    if kwargs["password"]:
 | 
			
		||||
        config += '  password: "{0}"\n\nota:\n  password: "{0}"\n'.format(
 | 
			
		||||
            kwargs["password"]
 | 
			
		||||
    # Configure API
 | 
			
		||||
    if "password" in kwargs:
 | 
			
		||||
        config += '  password: "{0}"\n'.format(kwargs["password"])
 | 
			
		||||
 | 
			
		||||
    # Configure OTA
 | 
			
		||||
    config += "\nota:\n"
 | 
			
		||||
    if "ota_password" in kwargs:
 | 
			
		||||
        config += '  password: "{0}"'.format(kwargs["ota_password"])
 | 
			
		||||
    elif "password" in kwargs:
 | 
			
		||||
        config += '  password: "{0}"'.format(kwargs["password"])
 | 
			
		||||
 | 
			
		||||
    # Configuring wifi
 | 
			
		||||
    config += "\n\nwifi:\n"
 | 
			
		||||
 | 
			
		||||
    if "ssid" in kwargs:
 | 
			
		||||
        config += """  ssid: "{ssid}"
 | 
			
		||||
  password: "{psk}"
 | 
			
		||||
""".format(
 | 
			
		||||
            **kwargs
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        config += "\nota:\n"
 | 
			
		||||
        config += """  # ssid: "My SSID"
 | 
			
		||||
  # password: "mypassword"
 | 
			
		||||
 | 
			
		||||
  networks:
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
    config += """
 | 
			
		||||
  # Enable fallback hotspot (captive portal) in case wifi connection fails
 | 
			
		||||
  ap:
 | 
			
		||||
    ssid: "{fallback_name}"
 | 
			
		||||
    password: "{fallback_psk}"
 | 
			
		||||
 | 
			
		||||
captive_portal:
 | 
			
		||||
""".format(
 | 
			
		||||
        **kwargs
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
@@ -97,9 +117,9 @@ def wizard_write(path, **kwargs):
 | 
			
		||||
    name = kwargs["name"]
 | 
			
		||||
    board = kwargs["board"]
 | 
			
		||||
 | 
			
		||||
    kwargs["ssid"] = sanitize_double_quotes(kwargs["ssid"])
 | 
			
		||||
    kwargs["psk"] = sanitize_double_quotes(kwargs["psk"])
 | 
			
		||||
    kwargs["password"] = sanitize_double_quotes(kwargs["password"])
 | 
			
		||||
    for key in ("ssid", "psk", "password", "ota_password"):
 | 
			
		||||
        if key in kwargs:
 | 
			
		||||
            kwargs[key] = sanitize_double_quotes(kwargs[key])
 | 
			
		||||
 | 
			
		||||
    if "platform" not in kwargs:
 | 
			
		||||
        kwargs["platform"] = "ESP8266" if board in ESP8266_BOARD_PINS else "ESP32"
 | 
			
		||||
 
 | 
			
		||||
@@ -11,3 +11,4 @@ ifaddr==0.1.7
 | 
			
		||||
platformio==5.1.1
 | 
			
		||||
esptool==2.8
 | 
			
		||||
click==7.1.2
 | 
			
		||||
esphome-dashboard==20210622.0
 | 
			
		||||
 
 | 
			
		||||
@@ -29,9 +29,10 @@ output:
 | 
			
		||||
    id: built_in_led
 | 
			
		||||
 | 
			
		||||
esp32_ble:
 | 
			
		||||
  server:
 | 
			
		||||
    manufacturer: "ESPHome"
 | 
			
		||||
    model: "Test5"
 | 
			
		||||
 | 
			
		||||
esp32_ble_server:
 | 
			
		||||
  manufacturer: "ESPHome"
 | 
			
		||||
  model: "Test5"
 | 
			
		||||
 | 
			
		||||
esp32_improv:
 | 
			
		||||
  authorizer: io0_button
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user