diff --git a/esphome/components/mitsubishi_itp/button/__init__.py b/esphome/components/mitsubishi_itp/button/__init__.py new file mode 100644 index 000000000000..f30ff71f8569 --- /dev/null +++ b/esphome/components/mitsubishi_itp/button/__init__.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.const import ( + ENTITY_CATEGORY_CONFIG, +) +from esphome.core import coroutine +from ...mitsubishi_itp.climate import ( + CONF_MITSUBISHI_ITP_ID, + mitsubishi_itp_ns, + MitsubishiUART, +) + +CONF_FILTER_RESET_BUTTON = "filter_reset_button" + +FilterResetButton = mitsubishi_itp_ns.class_( + "FilterResetButton", button.Button, cg.Component +) + +BUTTONS = { + CONF_FILTER_RESET_BUTTON: button.button_schema( + FilterResetButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:restore", + ), +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MITSUBISHI_ITP_ID): cv.use_id(MitsubishiUART), + } +).extend( + { + cv.Optional(button_designator): button_schema + for button_designator, button_schema in BUTTONS.items() + } +) + + +@coroutine +async def to_code(config): + muart_component = await cg.get_variable(config[CONF_MITSUBISHI_ITP_ID]) + + # Buttons + for button_designator, _ in BUTTONS.items(): + button_conf = config[button_designator] + button_component = await button.new_button(button_conf) + await cg.register_component(button_component, button_conf) + await cg.register_parented(button_component, muart_component) diff --git a/esphome/components/mitsubishi_itp/muart_button.h b/esphome/components/mitsubishi_itp/button/muart_button.h similarity index 94% rename from esphome/components/mitsubishi_itp/muart_button.h rename to esphome/components/mitsubishi_itp/button/muart_button.h index e95e1b8ce784..1884bc2f0341 100644 --- a/esphome/components/mitsubishi_itp/muart_button.h +++ b/esphome/components/mitsubishi_itp/button/muart_button.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/components/button/button.h" -#include "mitsubishi_uart.h" +#include "../mitsubishi_uart.h" namespace esphome { namespace mitsubishi_itp { diff --git a/esphome/components/mitsubishi_itp/climate.py b/esphome/components/mitsubishi_itp/climate.py index 0d1a22c07e3f..44380db9a6a1 100644 --- a/esphome/components/mitsubishi_itp/climate.py +++ b/esphome/components/mitsubishi_itp/climate.py @@ -3,30 +3,15 @@ from esphome.components import ( climate, uart, - time, sensor, - binary_sensor, - button, - text_sensor, - select, + time, ) from esphome.const import ( CONF_CUSTOM_FAN_MODES, CONF_ID, - CONF_NAME, - CONF_OUTDOOR_TEMPERATURE, - CONF_SENSORS, CONF_SUPPORTED_FAN_MODES, CONF_SUPPORTED_MODES, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_FREQUENCY, - DEVICE_CLASS_HUMIDITY, - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_NONE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_HERTZ, - UNIT_PERCENT, + CONF_TIME_ID, ) from esphome.core import coroutine @@ -43,26 +28,10 @@ ] DEPENDENCIES = [ "uart", - "climate", ] CONF_UART_HEATPUMP = "uart_heatpump" CONF_UART_THERMOSTAT = "uart_thermostat" -CONF_TIME_SOURCE = "time_source" - -CONF_THERMOSTAT_TEMPERATURE = "thermostat_temperature" -CONF_THERMOSTAT_HUMIDITY = "thermostat_humidity" -CONF_THERMOSTAT_BATTERY = "thermostat_battery" -CONF_ERROR_CODE = "error_code" -CONF_ISEE_STATUS = "isee_status" - -CONF_SELECTS = "selects" -CONF_TEMPERATURE_SOURCE_SELECT = "temperature_source_select" # This is to create a Select object for selecting a source -CONF_VANE_POSITION_SELECT = "vane_position_select" -CONF_HORIZONTAL_VANE_POSITION_SELECT = "horizontal_vane_position_select" - -CONF_BUTTONS = "buttons" -CONF_FILTER_RESET_BUTTON = "filter_reset_button" CONF_TEMPERATURE_SOURCES = ( "temperature_sources" # This is for specifying additional sources @@ -79,47 +48,20 @@ MitsubishiUART = mitsubishi_itp_ns.class_( "MitsubishiUART", cg.PollingComponent, climate.Climate ) - -TemperatureSourceSelect = mitsubishi_itp_ns.class_( - "TemperatureSourceSelect", select.Select -) -VanePositionSelect = mitsubishi_itp_ns.class_("VanePositionSelect", select.Select) -HorizontalVanePositionSelect = mitsubishi_itp_ns.class_( - "HorizontalVanePositionSelect", select.Select -) - -FilterResetButton = mitsubishi_itp_ns.class_( - "FilterResetButton", button.Button, cg.Component -) +CONF_MITSUBISHI_ITP_ID = "mitsubishi_itp_id" DEFAULT_CLIMATE_MODES = ["OFF", "HEAT", "DRY", "COOL", "FAN_ONLY", "HEAT_COOL"] DEFAULT_FAN_MODES = ["AUTO", "QUIET", "LOW", "MEDIUM", "HIGH"] CUSTOM_FAN_MODES = {"VERYHIGH": mitsubishi_itp_ns.FAN_MODE_VERYHIGH} -VANE_POSITIONS = ["Auto", "1", "2", "3", "4", "5", "Swing"] -HORIZONTAL_VANE_POSITIONS = ["Auto", "<<", "<", "|", ">", ">>", "<>", "Swing"] - -INTERNAL_TEMPERATURE_SOURCE_OPTIONS = [ - mitsubishi_itp_ns.TEMPERATURE_SOURCE_INTERNAL -] # These will always be available validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) -BASE_SCHEMA = climate.CLIMATE_SCHEMA.extend( +CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend( { cv.GenerateID(CONF_ID): cv.declare_id(MitsubishiUART), cv.Required(CONF_UART_HEATPUMP): cv.use_id(uart.UARTComponent), cv.Optional(CONF_UART_THERMOSTAT): cv.use_id(uart.UARTComponent), - cv.Optional(CONF_TIME_SOURCE): cv.use_id(time.RealTimeClock), - # Overwrite name from ENTITY_BASE_SCHEMA with "Climate" as default - cv.Optional(CONF_NAME, default="Climate"): cv.Any( - cv.All( - cv.none, - cv.requires_friendly_name( - "Name cannot be None when esphome->friendly_name is not set!" - ), - ), - cv.string, - ), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), cv.Optional( CONF_SUPPORTED_MODES, default=DEFAULT_CLIMATE_MODES ): cv.ensure_list(climate.validate_climate_mode), @@ -137,184 +79,6 @@ } ).extend(cv.polling_component_schema(DEFAULT_POLLING_INTERVAL)) -# TODO Storing the registration function here seems weird, but I can't figure out how to determine schema type later -SENSORS = dict[str, tuple[str, cv.Schema, callable]]( - { - CONF_THERMOSTAT_TEMPERATURE: ( - "Thermostat Temperature", - sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=1, - ), - sensor.register_sensor, - ), - CONF_OUTDOOR_TEMPERATURE: ( - "Outdoor Temperature", - sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=1, - icon="mdi:sun-thermometer-outline", - ), - sensor.register_sensor, - ), - CONF_THERMOSTAT_HUMIDITY: ( - "Thermostat Humidity", - sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, - accuracy_decimals=0, - ), - sensor.register_sensor, - ), - CONF_THERMOSTAT_BATTERY: ( - "Thermostat Battery", - text_sensor.text_sensor_schema( - icon="mdi:battery", - ), - text_sensor.register_text_sensor, - ), - "compressor_frequency": ( - "Compressor Frequency", - sensor.sensor_schema( - unit_of_measurement=UNIT_HERTZ, - device_class=DEVICE_CLASS_FREQUENCY, - state_class=STATE_CLASS_MEASUREMENT, - ), - sensor.register_sensor, - ), - "actual_fan": ( - "Actual Fan Speed", - text_sensor.text_sensor_schema( - icon="mdi:fan", - ), - text_sensor.register_text_sensor, - ), - "filter_status": ( - "Filter Status", - binary_sensor.binary_sensor_schema( - device_class="problem", icon="mdi:air-filter" - ), - binary_sensor.register_binary_sensor, - ), - "defrost": ( - "Defrost", - binary_sensor.binary_sensor_schema(icon="mdi:snowflake-melt"), - binary_sensor.register_binary_sensor, - ), - "preheat": ( - "Preheat", - binary_sensor.binary_sensor_schema(icon="mdi:heating-coil"), - binary_sensor.register_binary_sensor, - ), - "standby": ( - "Standby", - binary_sensor.binary_sensor_schema(icon="mdi:pause-circle-outline"), - binary_sensor.register_binary_sensor, - ), - CONF_ISEE_STATUS: ( - "i-see Status", - binary_sensor.binary_sensor_schema(icon="mdi:eye"), - binary_sensor.register_binary_sensor, - ), - CONF_ERROR_CODE: ( - "Error Code", - text_sensor.text_sensor_schema(icon="mdi:alert-circle-outline"), - text_sensor.register_text_sensor, - ), - } -) - -SENSORS_SCHEMA = cv.All( - { - cv.Optional( - sensor_designator, - default={"name": f"{sensor_name}", "disabled_by_default": "true"}, - ): sensor_schema - for sensor_designator, ( - sensor_name, - sensor_schema, - registration_function, - ) in SENSORS.items() - } -) - -SELECTS = { - CONF_TEMPERATURE_SOURCE_SELECT: ( - "Temperature Source", - select.select_schema( - TemperatureSourceSelect, - entity_category=ENTITY_CATEGORY_CONFIG, - icon="mdi:thermometer-check", - ), - INTERNAL_TEMPERATURE_SOURCE_OPTIONS, - ), - CONF_VANE_POSITION_SELECT: ( - "Vane Position", - select.select_schema( - VanePositionSelect, - entity_category=ENTITY_CATEGORY_NONE, - icon="mdi:arrow-expand-vertical", - ), - VANE_POSITIONS, - ), - CONF_HORIZONTAL_VANE_POSITION_SELECT: ( - "Horizontal Vane Position", - select.select_schema( - HorizontalVanePositionSelect, - entity_category=ENTITY_CATEGORY_NONE, - icon="mdi:arrow-expand-horizontal", - ), - HORIZONTAL_VANE_POSITIONS, - ), -} - -SELECTS_SCHEMA = cv.All( - { - cv.Optional( - select_designator, default={"name": f"{select_name}"} - ): select_schema - for select_designator, ( - select_name, - select_schema, - select_options, - ) in SELECTS.items() - } -) - -BUTTONS = { - CONF_FILTER_RESET_BUTTON: ( - "Filter Reset", - button.button_schema( - FilterResetButton, - entity_category=ENTITY_CATEGORY_CONFIG, - icon="mdi:restore", - ), - ) -} - -BUTTONS_SCHEMA = cv.All( - { - cv.Optional( - button_designator, default={"name": f"{button_name}"} - ): button_schema - for button_designator, (button_name, button_schema) in BUTTONS.items() - } -) - - -CONFIG_SCHEMA = BASE_SCHEMA.extend( - { - cv.Optional(CONF_SENSORS, default={}): SENSORS_SCHEMA, - cv.Optional(CONF_SELECTS, default={}): SELECTS_SCHEMA, - cv.Optional(CONF_BUTTONS, default={}): BUTTONS_SCHEMA, - } -) - def final_validate(config): schema = uart.final_validate_device_schema( @@ -357,16 +121,14 @@ async def to_code(config): # Register thermostat with MUART ts_uart_component = await cg.get_variable(config[CONF_UART_THERMOSTAT]) cg.add(getattr(muart_component, "set_thermostat_uart")(ts_uart_component)) - # Add sensor as source - SELECTS[CONF_TEMPERATURE_SOURCE_SELECT][2].append("Thermostat") # If RTC defined - if CONF_TIME_SOURCE in config: - rtc_component = await cg.get_variable(config[CONF_TIME_SOURCE]) + if CONF_TIME_ID in config: + rtc_component = await cg.get_variable(config[CONF_TIME_ID]) cg.add(getattr(muart_component, "set_time_source")(rtc_component)) elif CONF_UART_THERMOSTAT in config and config.get(CONF_ENHANCED_MHK_SUPPORT): raise cv.RequiredFieldInvalid( - f"{CONF_TIME_SOURCE} is required if {CONF_ENHANCED_MHK_SUPPORT} is set." + f"{CONF_TIME_ID} is required if {CONF_ENHANCED_MHK_SUPPORT} is set." ) # Traits @@ -381,41 +143,12 @@ async def to_code(config): if CONF_CUSTOM_FAN_MODES in config: cg.add(traits.set_supported_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) - # Sensors - - for sensor_designator, ( - _, - _, - registration_function, - ) in SENSORS.items(): - # Only add the thermostat temp if we have a TS_UART - if (CONF_UART_THERMOSTAT not in config) and ( - sensor_designator - in [ - CONF_THERMOSTAT_TEMPERATURE, - CONF_THERMOSTAT_HUMIDITY, - CONF_THERMOSTAT_BATTERY, - ] - ): - continue - - sensor_conf = config[CONF_SENSORS][sensor_designator] - sensor_component = cg.new_Pvariable(sensor_conf[CONF_ID]) - - await registration_function(sensor_component, sensor_conf) - - cg.add( - getattr(muart_component, f"set_{sensor_designator}_sensor")( - sensor_component - ) - ) - - # Selects - # Add additional configured temperature sensors to the select menu for ts_id in config[CONF_TEMPERATURE_SOURCES]: ts = await cg.get_variable(ts_id) - SELECTS[CONF_TEMPERATURE_SOURCE_SELECT][2].append(ts.get_name()) + cg.add( + getattr(muart_component, "register_temperature_source")(ts.get_name().str()) + ) cg.add( getattr(ts, "add_on_state_callback")( # TODO: Is there anyway to do this without a raw expression? @@ -425,33 +158,6 @@ async def to_code(config): ) ) - # Register selects - for select_designator, ( - _, - _, - select_options, - ) in SELECTS.items(): - select_conf = config[CONF_SELECTS][select_designator] - select_component = cg.new_Pvariable(select_conf[CONF_ID]) - cg.add(getattr(muart_component, f"set_{select_designator}")(select_component)) - await cg.register_parented(select_component, muart_component) - - # For temperature source select, skip registration if there are less than two sources - if select_designator == CONF_TEMPERATURE_SOURCE_SELECT: - if len(SELECTS[CONF_TEMPERATURE_SOURCE_SELECT][2]) < 2: - continue - - await select.register_select( - select_component, select_conf, options=select_options - ) - - # Buttons - for button_designator, (_, _) in BUTTONS.items(): - button_conf = config[CONF_BUTTONS][button_designator] - button_component = await button.new_button(button_conf) - await cg.register_component(button_component, button_conf) - await cg.register_parented(button_component, muart_component) - # Debug Settings if dam_conf := config.get(CONF_DISABLE_ACTIVE_MODE): cg.add(getattr(muart_component, "set_active_mode")(not dam_conf)) diff --git a/esphome/components/mitsubishi_itp/mitsubishi_uart-packetprocessing.cpp b/esphome/components/mitsubishi_itp/mitsubishi_uart-packetprocessing.cpp index f0b641887f83..ad62be071ff5 100644 --- a/esphome/components/mitsubishi_itp/mitsubishi_uart-packetprocessing.cpp +++ b/esphome/components/mitsubishi_itp/mitsubishi_uart-packetprocessing.cpp @@ -158,65 +158,71 @@ void MitsubishiUART::process_packet(const SettingsGetResponsePacket &packet) { publish_on_update_ |= fan_changed; - // TODO: It would probably be nice to have the enum->string mapping defined somewhere to avoid typos/errors - const std::string old_vane_position = vane_position_select_->state; - switch (packet.get_vane()) { - case SettingsSetRequestPacket::VANE_AUTO: - vane_position_select_->state = "Auto"; - break; - case SettingsSetRequestPacket::VANE_1: - vane_position_select_->state = "1"; - break; - case SettingsSetRequestPacket::VANE_2: - vane_position_select_->state = "2"; - break; - case SettingsSetRequestPacket::VANE_3: - vane_position_select_->state = "3"; - break; - case SettingsSetRequestPacket::VANE_4: - vane_position_select_->state = "4"; - break; - case SettingsSetRequestPacket::VANE_5: - vane_position_select_->state = "5"; - break; - case SettingsSetRequestPacket::VANE_SWING: - vane_position_select_->state = "Swing"; - break; - default: - ESP_LOGW(TAG, "Vane in unknown position %x", packet.get_vane()); + // If we don't have a vane position select, there is no where to report this data, and no need to parse it + if (vane_position_select_) { + // TODO: It would probably be nice to have the enum->string mapping defined somewhere to avoid typos/errors + const std::string old_vane_position = vane_position_select_->state; + switch (packet.get_vane()) { + case SettingsSetRequestPacket::VANE_AUTO: + vane_position_select_->state = "Auto"; + break; + case SettingsSetRequestPacket::VANE_1: + vane_position_select_->state = "1"; + break; + case SettingsSetRequestPacket::VANE_2: + vane_position_select_->state = "2"; + break; + case SettingsSetRequestPacket::VANE_3: + vane_position_select_->state = "3"; + break; + case SettingsSetRequestPacket::VANE_4: + vane_position_select_->state = "4"; + break; + case SettingsSetRequestPacket::VANE_5: + vane_position_select_->state = "5"; + break; + case SettingsSetRequestPacket::VANE_SWING: + vane_position_select_->state = "Swing"; + break; + default: + ESP_LOGW(TAG, "Vane in unknown position %x", packet.get_vane()); + } + publish_on_update_ |= (old_vane_position != vane_position_select_->state); } - publish_on_update_ |= (old_vane_position != vane_position_select_->state); - const std::string old_horizontal_vane_position = horizontal_vane_position_select_->state; - switch (packet.get_horizontal_vane()) { - case SettingsSetRequestPacket::HV_AUTO: - horizontal_vane_position_select_->state = "Auto"; - break; - case SettingsSetRequestPacket::HV_LEFT_FULL: - horizontal_vane_position_select_->state = "<<"; - break; - case SettingsSetRequestPacket::HV_LEFT: - horizontal_vane_position_select_->state = "<"; - break; - case SettingsSetRequestPacket::HV_CENTER: - horizontal_vane_position_select_->state = "|"; - break; - case SettingsSetRequestPacket::HV_RIGHT: - horizontal_vane_position_select_->state = ">"; - break; - case SettingsSetRequestPacket::HV_RIGHT_FULL: - horizontal_vane_position_select_->state = ">>"; - break; - case SettingsSetRequestPacket::HV_SPLIT: - horizontal_vane_position_select_->state = "<>"; - break; - case SettingsSetRequestPacket::HV_SWING: - horizontal_vane_position_select_->state = "Swing"; - break; - default: - ESP_LOGW(TAG, "Vane in unknown horizontal position %x", packet.get_horizontal_vane()); + // If we don't have a horizontal vane position select, there is no where to report this data, and no need to parse it + if (horizontal_vane_position_select_) { + const std::string old_horizontal_vane_position = horizontal_vane_position_select_->state; + switch (packet.get_horizontal_vane()) { + case SettingsSetRequestPacket::HV_AUTO: + horizontal_vane_position_select_->state = "Auto"; + break; + case SettingsSetRequestPacket::HV_LEFT_FULL: + horizontal_vane_position_select_->state = "<<"; + break; + case SettingsSetRequestPacket::HV_LEFT: + horizontal_vane_position_select_->state = "<"; + break; + case SettingsSetRequestPacket::HV_CENTER: + horizontal_vane_position_select_->state = "|"; + break; + case SettingsSetRequestPacket::HV_RIGHT: + horizontal_vane_position_select_->state = ">"; + break; + case SettingsSetRequestPacket::HV_RIGHT_FULL: + horizontal_vane_position_select_->state = ">>"; + break; + case SettingsSetRequestPacket::HV_SPLIT: + horizontal_vane_position_select_->state = "<>"; + break; + case SettingsSetRequestPacket::HV_SWING: + horizontal_vane_position_select_->state = "Swing"; + break; + default: + ESP_LOGW(TAG, "Vane in unknown horizontal position %x", packet.get_horizontal_vane()); + } + publish_on_update_ |= (old_horizontal_vane_position != horizontal_vane_position_select_->state); } - publish_on_update_ |= (old_horizontal_vane_position != horizontal_vane_position_select_->state); } void MitsubishiUART::process_packet(const CurrentTempGetResponsePacket &packet) { @@ -228,7 +234,7 @@ void MitsubishiUART::process_packet(const CurrentTempGetResponsePacket &packet) publish_on_update_ |= (old_current_temperature != current_temperature); - if (!std::isnan(packet.get_outdoor_temp())) { + if (outdoor_temperature_sensor_ && !std::isnan(packet.get_outdoor_temp())) { const float old_outdoor_temperature = outdoor_temperature_sensor_->raw_state; outdoor_temperature_sensor_->raw_state = packet.get_outdoor_temp(); publish_on_update_ |= (old_outdoor_temperature != outdoor_temperature_sensor_->raw_state); @@ -336,23 +342,27 @@ void MitsubishiUART::process_packet(const ErrorStateGetResponsePacket &packet) { ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str()); route_packet_(packet); - std::string old_error_code = error_code_sensor_->raw_state; - - // TODO: Include friendly text from JSON, somehow. - if (!packet.error_present()) { - error_code_sensor_->raw_state = "No Error Reported"; - } else if (auto raw_code = packet.get_raw_short_code() != 0x00) { - // Not that it matters, but good for validation I guess. - if ((raw_code & 0x1F) > 0x15) { - ESP_LOGW(TAG, "Error short code %x had invalid low bits. This is an IT protocol violation!", raw_code); + // Only worry about this if we have error_code_sensor_ defined + // TODO: Should we log a warning with error codes even if no sensor? + if (error_code_sensor_) { + std::string old_error_code = error_code_sensor_->raw_state; + + // TODO: Include friendly text from JSON, somehow. + if (!packet.error_present()) { + error_code_sensor_->raw_state = "No Error Reported"; + } else if (auto raw_code = packet.get_raw_short_code() != 0x00) { + // Not that it matters, but good for validation I guess. + if ((raw_code & 0x1F) > 0x15) { + ESP_LOGW(TAG, "Error short code %x had invalid low bits. This is an IT protocol violation!", raw_code); + } + + error_code_sensor_->raw_state = "Error " + packet.get_short_code(); + } else { + error_code_sensor_->raw_state = "Error " + to_string(packet.get_error_code()); } - error_code_sensor_->raw_state = "Error " + packet.get_short_code(); - } else { - error_code_sensor_->raw_state = "Error " + to_string(packet.get_error_code()); + publish_on_update_ |= (old_error_code != error_code_sensor_->raw_state); } - - publish_on_update_ |= (old_error_code != error_code_sensor_->raw_state); } void MitsubishiUART::process_packet(const SettingsSetRequestPacket &packet) { diff --git a/esphome/components/mitsubishi_itp/mitsubishi_uart.cpp b/esphome/components/mitsubishi_itp/mitsubishi_uart.cpp index 8738b2675d15..812247376575 100644 --- a/esphome/components/mitsubishi_itp/mitsubishi_uart.cpp +++ b/esphome/components/mitsubishi_itp/mitsubishi_uart.cpp @@ -22,9 +22,12 @@ MitsubishiUART::MitsubishiUART(uart::UARTComponent *hp_uart_comp) // Used to restore state of previous MUART-specific settings (like temperature source or pass-thru mode) // Most other climate-state is preserved by the heatpump itself and will be retrieved after connection void MitsubishiUART::setup() { - // Populate select map - for (size_t index = 0; index < temperature_source_select_->traits.get_options().size(); index++) { - temp_select_map_[temperature_source_select_->traits.get_options()[index]] = index; + if (ts_uart_) { + temp_select_options_.push_back(TEMPERATURE_SOURCE_THERMOSTAT); + } + + if (temperature_source_select_) { + temperature_source_select_->traits.set_options(temp_select_options_); } // Using App.get_compilation_time() means these will get reset each time the firmware is updated, but this @@ -39,9 +42,9 @@ void MitsubishiUART::save_preferences_() { // currentTemperatureSource // Save the index of the value stored in currentTemperatureSource just in case we're temporarily using Internal - auto index = temp_select_map_.find(current_temperature_source_); - if (index != temp_select_map_.end()) { - prefs.currentTemperatureSourceIndex = index->second; + auto index = find(temp_select_options_.begin(), temp_select_options_.end(), current_temperature_source_); + if (index != temp_select_options_.end()) { + prefs.currentTemperatureSourceIndex = index - temp_select_options_.begin(); } preferences_.save(&prefs); @@ -52,22 +55,27 @@ void MitsubishiUART::restore_preferences_() { MUARTPreferences prefs; if (preferences_.load(&prefs)) { // currentTemperatureSource - if (prefs.currentTemperatureSourceIndex.has_value() && - temperature_source_select_->has_index(prefs.currentTemperatureSourceIndex.value()) && - temperature_source_select_->at(prefs.currentTemperatureSourceIndex.value()).has_value()) { - current_temperature_source_ = temperature_source_select_->at(prefs.currentTemperatureSourceIndex.value()).value(); - temperature_source_select_->publish_state(current_temperature_source_); - ESP_LOGCONFIG(TAG, "Preferences loaded."); - } else { - ESP_LOGCONFIG(TAG, "Preferences loaded, but unsuitable values."); - current_temperature_source_ = TEMPERATURE_SOURCE_INTERNAL; - temperature_source_select_->publish_state(TEMPERATURE_SOURCE_INTERNAL); + if (temperature_source_select_) { + if (prefs.currentTemperatureSourceIndex.has_value() && + temperature_source_select_->has_index(prefs.currentTemperatureSourceIndex.value()) && + temperature_source_select_->at(prefs.currentTemperatureSourceIndex.value()).has_value()) { + current_temperature_source_ = + temperature_source_select_->at(prefs.currentTemperatureSourceIndex.value()).value(); + temperature_source_select_->publish_state(current_temperature_source_); + ESP_LOGCONFIG(TAG, "Preferences loaded."); + } else { + ESP_LOGCONFIG(TAG, "Preferences loaded, but unsuitable values."); + current_temperature_source_ = TEMPERATURE_SOURCE_INTERNAL; + temperature_source_select_->publish_state(TEMPERATURE_SOURCE_INTERNAL); + } } } else { // TODO: Shouldn't need to define setting all these defaults twice ESP_LOGCONFIG(TAG, "Preferences not loaded."); current_temperature_source_ = TEMPERATURE_SOURCE_INTERNAL; - temperature_source_select_->publish_state(TEMPERATURE_SOURCE_INTERNAL); + if (temperature_source_select_) { + temperature_source_select_->publish_state(TEMPERATURE_SOURCE_INTERNAL); + } } } @@ -95,19 +103,20 @@ void MitsubishiUART::loop() { if (ts_bridge_) ts_bridge_->loop(); - // If it's been too long since we received a temperature update (and we're not set to Internal) - if (((millis() - last_received_temperature_) > TEMPERATURE_SOURCE_TIMEOUT_MS) && - (temperature_source_select_->has_option(TEMPERATURE_SOURCE_INTERNAL)) && - (temperature_source_select_->state != TEMPERATURE_SOURCE_INTERNAL)) { - ESP_LOGW(TAG, "No temperature received from %s for %lu milliseconds, reverting to Internal source", - current_temperature_source_.c_str(), (unsigned long) TEMPERATURE_SOURCE_TIMEOUT_MS); - // Set the select to show Internal (but do not change currentTemperatureSource) - temperature_source_select_->publish_state(TEMPERATURE_SOURCE_INTERNAL); - // Send a packet to the heat pump to tell it to switch to internal temperature sensing - IFACTIVE(hp_bridge_.send_packet(RemoteTemperatureSetRequestPacket().use_internal_temperature());) + // If no temperature_source_select_ has been defined, Internal will always be used, no need to check on this + if (temperature_source_select_) { + // If it's been too long since we received a temperature update (and we're not set to Internal) + if (((millis() - last_received_temperature_) > TEMPERATURE_SOURCE_TIMEOUT_MS) && + (temperature_source_select_->has_option(TEMPERATURE_SOURCE_INTERNAL)) && + (temperature_source_select_->state != TEMPERATURE_SOURCE_INTERNAL)) { + ESP_LOGW(TAG, "No temperature received from %s for %lu milliseconds, reverting to Internal source", + current_temperature_source_.c_str(), (unsigned long) TEMPERATURE_SOURCE_TIMEOUT_MS); + // Set the select to show Internal (but do not change currentTemperatureSource) + temperature_source_select_->publish_state(TEMPERATURE_SOURCE_INTERNAL); + // Send a packet to the heat pump to tell it to switch to internal temperature sensing + IFACTIVE(hp_bridge_.send_packet(RemoteTemperatureSetRequestPacket().use_internal_temperature());) + } } - // - // Send packet to HP to tell it to use internal temp sensor } void MitsubishiUART::dump_config() { @@ -189,8 +198,13 @@ void MitsubishiUART::update() { void MitsubishiUART::do_publish_() { publish_state(); - vane_position_select_->publish_state(vane_position_select_->state); - horizontal_vane_position_select_->publish_state(horizontal_vane_position_select_->state); + if (vane_position_select_) { + vane_position_select_->publish_state(vane_position_select_->state); + } + + if (horizontal_vane_position_select_) { + horizontal_vane_position_select_->publish_state(horizontal_vane_position_select_->state); + } save_preferences_(); // We can save this every time we publish as writes to flash are by default collected and // delayed @@ -230,11 +244,21 @@ void MitsubishiUART::do_publish_() { } // Binary sensors automatically dedup publishes (I think) and so will only actually publish on change - filter_status_sensor_->publish_state(filter_status_sensor_->state); - defrost_sensor_->publish_state(defrost_sensor_->state); - preheat_sensor_->publish_state(preheat_sensor_->state); - standby_sensor_->publish_state(standby_sensor_->state); - isee_status_sensor_->publish_state(isee_status_sensor_->state); + if (filter_status_sensor_) { + filter_status_sensor_->publish_state(filter_status_sensor_->state); + } + if (defrost_sensor_) { + defrost_sensor_->publish_state(defrost_sensor_->state); + } + if (preheat_sensor_) { + preheat_sensor_->publish_state(preheat_sensor_->state); + } + if (standby_sensor_) { + standby_sensor_->publish_state(standby_sensor_->state); + } + if (isee_status_sensor_) { + isee_status_sensor_->publish_state(isee_status_sensor_->state); + } } bool MitsubishiUART::select_temperature_source(const std::string &state) { @@ -315,6 +339,10 @@ bool MitsubishiUART::select_horizontal_vane_position(const std::string &state) { return true; } +void MitsubishiUART::register_temperature_source(std::string temperature_source_name) { + temp_select_options_.push_back(temperature_source_name); +} + // Called by temperature_source sensors to report values. Will only take action if the currentTemperatureSource // matches the incoming source. Specifically this means that we are not storing any values // for sensors other than the current source, and selecting a different source won't have any @@ -336,7 +364,7 @@ void MitsubishiUART::temperature_source_report(const std::string &temperature_so hp_bridge_.send_packet(pkt);) // If we've changed the select to reflect a temporary reversion to a different source, change it back. - if (temperature_source_select_->state != temperature_source) { + if (temperature_source_select_ && temperature_source_select_->state != temperature_source) { ESP_LOGI(TAG, "Temperature received, switching back to %s as source.", temperature_source.c_str()); temperature_source_select_->publish_state(temperature_source); } diff --git a/esphome/components/mitsubishi_itp/mitsubishi_uart.h b/esphome/components/mitsubishi_itp/mitsubishi_uart.h index a80092430483..81e741223ee5 100644 --- a/esphome/components/mitsubishi_itp/mitsubishi_uart.h +++ b/esphome/components/mitsubishi_itp/mitsubishi_uart.h @@ -25,10 +25,10 @@ const float MUART_TEMPERATURE_STEP = 0.5; const std::string FAN_MODE_VERYHIGH = "Very High"; const std::string TEMPERATURE_SOURCE_INTERNAL = "Internal"; -const uint32_t TEMPERATURE_SOURCE_TIMEOUT_MS = 420000; // (7min) The heatpump will revert on its own in ~10min - const std::string TEMPERATURE_SOURCE_THERMOSTAT = "Thermostat"; +const uint32_t TEMPERATURE_SOURCE_TIMEOUT_MS = 420000; // (7min) The heatpump will revert on its own in ~10min + // These are named to match with set fan speeds where possible. "Very Low" is a special speed // for e.g. preheating or thermal off const std::array ACTUAL_FAN_SPEED_NAMES = {"Off", "Very Low", "Low", "Medium", @@ -95,6 +95,8 @@ class MitsubishiUART : public PollingComponent, public climate::Climate, public bool select_vane_position(const std::string &state); bool select_horizontal_vane_position(const std::string &state); + // Adds an option to temperature_source_select_ + void register_temperature_source(std::string temperature_source_name); // Used by external sources to report a temperature void temperature_source_report(const std::string &temperature_source, const float &v); @@ -198,12 +200,13 @@ class MitsubishiUART : public PollingComponent, public climate::Climate, public text_sensor::TextSensor *thermostat_battery_sensor_ = nullptr; // Selects - select::Select *temperature_source_select_; - select::Select *vane_position_select_; - select::Select *horizontal_vane_position_select_; + select::Select *temperature_source_select_ = nullptr; + select::Select *vane_position_select_ = nullptr; + select::Select *horizontal_vane_position_select_ = nullptr; // Temperature select extras - std::map temp_select_map_; // Used to map strings to indexes for preference storage + std::vector temp_select_options_ = { + TEMPERATURE_SOURCE_INTERNAL}; // Used to map strings to indexes for preference storage std::string current_temperature_source_ = TEMPERATURE_SOURCE_INTERNAL; uint32_t last_received_temperature_ = millis(); diff --git a/esphome/components/mitsubishi_itp/select/__init__.py b/esphome/components/mitsubishi_itp/select/__init__.py new file mode 100644 index 000000000000..fc133fe3a3b0 --- /dev/null +++ b/esphome/components/mitsubishi_itp/select/__init__.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ( + select, +) +from esphome.const import ( + CONF_ID, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_NONE, +) +from esphome.core import coroutine +from ...mitsubishi_itp.climate import ( + CONF_MITSUBISHI_ITP_ID, + mitsubishi_itp_ns, + MitsubishiUART, +) + +CONF_TEMPERATURE_SOURCE = ( + "temperature_source" # This is to create a Select object for selecting a source +) +CONF_VANE_POSITION = "vane_position" +CONF_HORIZONTAL_VANE_POSITION = "horizontal_vane_position" + +VANE_POSITIONS = ["Auto", "1", "2", "3", "4", "5", "Swing"] +HORIZONTAL_VANE_POSITIONS = ["Auto", "<<", "<", "|", ">", ">>", "<>", "Swing"] + +TemperatureSourceSelect = mitsubishi_itp_ns.class_( + "TemperatureSourceSelect", select.Select +) +VanePositionSelect = mitsubishi_itp_ns.class_("VanePositionSelect", select.Select) +HorizontalVanePositionSelect = mitsubishi_itp_ns.class_( + "HorizontalVanePositionSelect", select.Select +) + +SELECTS = { + CONF_TEMPERATURE_SOURCE: ( + select.select_schema( + TemperatureSourceSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:thermometer-check", + ), + [mitsubishi_itp_ns.TEMPERATURE_SOURCE_INTERNAL], + ), + CONF_VANE_POSITION: ( + select.select_schema( + VanePositionSelect, + entity_category=ENTITY_CATEGORY_NONE, + icon="mdi:arrow-expand-vertical", + ), + VANE_POSITIONS, + ), + CONF_HORIZONTAL_VANE_POSITION: ( + select.select_schema( + HorizontalVanePositionSelect, + entity_category=ENTITY_CATEGORY_NONE, + icon="mdi:arrow-expand-horizontal", + ), + HORIZONTAL_VANE_POSITIONS, + ), +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MITSUBISHI_ITP_ID): cv.use_id(MitsubishiUART), + } +).extend( + { + cv.Optional(select_designator): select_schema + for select_designator, ( + select_schema, + _, + ) in SELECTS.items() + } +) + + +@coroutine +async def to_code(config): + muart_component = await cg.get_variable(config[CONF_MITSUBISHI_ITP_ID]) + + # Register selects + for select_designator, ( + _, + select_options, + ) in SELECTS.items(): + if select_conf := config.get(select_designator): + select_component = cg.new_Pvariable(select_conf[CONF_ID]) + cg.add( + getattr(muart_component, f"set_{select_designator}_select")( + select_component + ) + ) + await cg.register_parented(select_component, muart_component) + + await select.register_select( + select_component, select_conf, options=select_options + ) diff --git a/esphome/components/mitsubishi_itp/muart_select.h b/esphome/components/mitsubishi_itp/select/muart_select.h similarity index 96% rename from esphome/components/mitsubishi_itp/muart_select.h rename to esphome/components/mitsubishi_itp/select/muart_select.h index 41d16550e97a..b886629a06dd 100644 --- a/esphome/components/mitsubishi_itp/muart_select.h +++ b/esphome/components/mitsubishi_itp/select/muart_select.h @@ -1,7 +1,7 @@ #pragma once #include "esphome/components/select/select.h" -#include "mitsubishi_uart.h" +#include "../mitsubishi_uart.h" namespace esphome { namespace mitsubishi_itp { diff --git a/esphome/components/mitsubishi_itp/sensor.py b/esphome/components/mitsubishi_itp/sensor.py new file mode 100644 index 000000000000..34521728bcd1 --- /dev/null +++ b/esphome/components/mitsubishi_itp/sensor.py @@ -0,0 +1,147 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ( + sensor, + binary_sensor, + text_sensor, +) +from esphome.const import ( + CONF_ID, + CONF_OUTDOOR_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_FREQUENCY, + DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_PERCENT, +) +from esphome.core import coroutine +from .climate import ( + CONF_MITSUBISHI_ITP_ID, + MitsubishiUART, +) + +CONF_THERMOSTAT_BATTERY = "thermostat_battery" +CONF_THERMOSTAT_HUMIDITY = "thermostat_humidity" +CONF_THERMOSTAT_TEMPERATURE = "thermostat_temperature" + + +CONF_ERROR_CODE = "error_code" +CONF_ISEE_STATUS = "isee_status" + +# TODO Storing the registration function here seems weird, but I can't figure out how to determine schema type later +SENSORS = dict[str, tuple[cv.Schema, callable]]( + { + "actual_fan": ( + text_sensor.text_sensor_schema( + icon="mdi:fan", + ), + text_sensor.register_text_sensor, + ), + "compressor_frequency": ( + sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, + ), + sensor.register_sensor, + ), + "defrost": ( + binary_sensor.binary_sensor_schema(icon="mdi:snowflake-melt"), + binary_sensor.register_binary_sensor, + ), + CONF_ERROR_CODE: ( + text_sensor.text_sensor_schema(icon="mdi:alert-circle-outline"), + text_sensor.register_text_sensor, + ), + "filter_status": ( + binary_sensor.binary_sensor_schema( + device_class="problem", icon="mdi:air-filter" + ), + binary_sensor.register_binary_sensor, + ), + CONF_ISEE_STATUS: ( + binary_sensor.binary_sensor_schema(icon="mdi:eye"), + binary_sensor.register_binary_sensor, + ), + CONF_OUTDOOR_TEMPERATURE: ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + icon="mdi:sun-thermometer-outline", + ), + sensor.register_sensor, + ), + "preheat": ( + binary_sensor.binary_sensor_schema(icon="mdi:heating-coil"), + binary_sensor.register_binary_sensor, + ), + "standby": ( + binary_sensor.binary_sensor_schema(icon="mdi:pause-circle-outline"), + binary_sensor.register_binary_sensor, + ), + CONF_THERMOSTAT_BATTERY: ( + text_sensor.text_sensor_schema( + icon="mdi:battery", + ), + text_sensor.register_text_sensor, + ), + CONF_THERMOSTAT_HUMIDITY: ( + sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=0, + ), + sensor.register_sensor, + ), + CONF_THERMOSTAT_TEMPERATURE: ( + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + accuracy_decimals=1, + ), + sensor.register_sensor, + ), + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MITSUBISHI_ITP_ID): cv.use_id(MitsubishiUART), + } +).extend( + { + cv.Optional(sensor_designator): sensor_schema + for sensor_designator, ( + sensor_schema, + _, + ) in SENSORS.items() + } +) + + +@coroutine +async def to_code(config): + muart_component = await cg.get_variable(config[CONF_MITSUBISHI_ITP_ID]) + + # Sensors + + for sensor_designator, ( + _, + registration_function, + ) in SENSORS.items(): + if sensor_conf := config.get(sensor_designator): + sensor_component = cg.new_Pvariable(sensor_conf[CONF_ID]) + + await registration_function(sensor_component, sensor_conf) + + cg.add( + getattr(muart_component, f"set_{sensor_designator}_sensor")( + sensor_component + ) + ) diff --git a/tests/components/mitsubishi_itp/common.yaml b/tests/components/mitsubishi_itp/common.yaml index 50a539f0897b..1247f3a31d15 100644 --- a/tests/components/mitsubishi_itp/common.yaml +++ b/tests/components/mitsubishi_itp/common.yaml @@ -34,9 +34,10 @@ uart: climate: - platform: mitsubishi_itp + name: Climate uart_heatpump: hp_uart uart_thermostat: tstat_uart - time_source: homeassistant_time + time_id: homeassistant_time update_interval: 12s temperature_sources: - fake_temp