Repository: hristo-atanasov/Tasmota-IRHVAC Branch: master Commit: b6b8dba5fcbf Files: 14 Total size: 81.4 KB Directory structure: gitextract_b4t92yan/ ├── .github/ │ └── workflows/ │ └── validate.yml ├── INFO.md ├── README.md ├── SERVICES.md ├── blueprints/ │ └── automation/ │ └── tasmota_irhvac/ │ └── climate_vane_control_tasmota-irhvac.yaml ├── custom_components/ │ └── tasmota_irhvac/ │ ├── __init__.py │ ├── climate.py │ ├── const.py │ ├── manifest.json │ └── services.yaml ├── examples/ │ ├── card_configuration.yaml │ ├── configuration.yaml │ └── scripts.yaml └── hacs.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/validate.yml ================================================ name: Validate on: push: pull_request: schedule: - cron: "0 0 * * *" workflow_dispatch: jobs: validate-hacs: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v3" - name: HACS validation uses: "hacs/action@main" with: category: "integration" ================================================ FILE: INFO.md ================================================ Tasmota-IRHVAC Home Assistant platform for controlling IR Air Conditioners via Tasmota IRHVAC command and compatible harware ================================================ FILE: README.md ================================================ [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) # Tasmota-IRHVAC Home Assistant platform for controlling IR Air Conditioners via Tasmota IRHVAC command and compatible hardware This is my new platform, that can **control hunderds of Air Conditioners**, out of the box, via **Tasmota IR transceivers**. It is based on the latest ***“tasmota-ircustom.bin” v8.1*** (tested successfully with Tasmota-ir v10.0.0). Currently it **works on Home Assistant 0.94 (may be some newer too) and from 0.103.x right to the latest 0.110.0 at this time** (Tested successfully with Home Assistant 2021.12.2) The schematics to make such Tasmota IR Transceiver is shown on the picture. I recommend not to put this 100ohm resistor that is marked with light blue X. If you’re planning to power the board with microUSB and you have pin named *VU* connect the IRLED to it instead of *VIN*. ![image1](/images/schematics.jpeg) Tasmota configuration looks like this: ![image2](/images/tasmota_config.jpeg) After configuration open Tasmota console, point your AC remote to the IR receiver and press the button for turning the AC on. If everything in the above steps is made right, you should see a line like this (example with Fujitsu Air Conditioner): ```javacript {'IrReceived': {'Protocol': 'FUJITSU_AC', 'Bits': 128, 'Data': '0x0x1463001010FE09304013003008002025', 'Repeat': 0, 'IRHVAC': {'Vendor': 'FUJITSU_AC', 'Model': 1, 'Power': 'On', 'Mode': 'fan_only', 'Celsius': 'On', 'Temp': 20, 'FanSpeed': 'Auto', 'SwingV': 'Off', 'SwingH': 'Off', 'Quiet': 'Off', 'Turbo': 'Off', 'Econo': 'Off', 'Light': 'Off', 'Filter': 'Off', 'Clean': 'Off', 'Beep': 'Off', 'Sleep': -1}}} ``` If vendor is not *‘Unknown’* and you see the *‘IRHVAC’* key, containing information, you can be sure that it will work for you. Next step is to download the files from this repo, get the folder named *"tasmota_irhvac"* and place it in your *"custom_components"* folder. Reastart Home Assistant! After restart add the config from *"configuration.yaml"* in your *"configuration.yaml"* file, but don’t save it yet, because you’ll need to replace all values with your speciffic AC values. Using your remote and the IR Transceiver do the following steps to find your AC values that you have to fill in. You can find these values by looking in the console for them. They will appear in the ‘IrReceived’ JSON line (mentioned earlier). Cycle trough all of your AC modes and write them in supported_modes. I have left some possible values commented. Cycle trough your fan speeds and and write them down in supported_fan_speeds If your AC doesnt support horizontal swinging remove *-"horizontal"* and *-"both"* from *supported_swing_list* Enter your *hvac_model* Change the *“min_temp”* and *“max_temp”* values with your AC min and max temp. *target_temp* is the initial target temp. 26 is default value and if you don’t want to change it, you can just remove the line. *away_temp* is the temp that will be set in away mode. If you don’t want to change it or you don’t need it you can remove that line. You can also remove all lines that doesn’t need to be changed and are marked with “optional”. Change the *name* with the desired name. After you finish with the config, save it and restart Home Assistant. Once restarted you can add in LovelaceUI a new *thermostat card* and select the newly integrated AC. This is a pic of 2 of my Tasmota IR transceivers, that I have mounted under my ACs so when using the ACs remote they have direct visual and update the state in Home Assistant (yes, it can do that too). ![image2](/images/multisensors.jpeg) As an addition you can add these 2 scripts from *scripts.yaml* in your *scripts.yaml* and use them to send all kind of HEX IR codes and RAW IR codes, by just naming your multisensors using room name (lowercase) and the word “Multisensor”. Like *“kitchenMultisensor”* or *“livingroomMultisensor”*. ```yaml ir_code: sequence: - data_template: payload: '{"Protocol":"{{ protocol }}","Bits": {{ bits }},"Data": 0x{{ data }}}' topic: 'cmnd/{{ room }}Multisensor/irsend' service: mqtt.publish ir_raw: sequence: - data_template: payload: '0, {{ data }}' topic: 'cmnd/{{ room }}Multisensor/irsend' service: mqtt.publish ``` You can then use these scripts, for the exmple, in a *button card*. Create a new card, put inside it the content of the *card_configuration.yaml*, change *bits:*, *data:*, *protocol:* and *room:* with your desired values and test it. :) ```yaml cards: - cards: - action: service color: white icon: 'mdi:power' name: Turn On Audio HEX service: action: ir_code data: bits: 12 data: A80 protocol: SONY room: kitchen domain: script style: - color: white - background: green - '--disabled-text-color': white type: 'custom:button-card' - action: service color: white icon: 'mdi:power' name: Turn Off Audio HEX service: action: ir_code data: bits: 12 data: E85 protocol: SONY room: kitchen domain: script style: - color: white - background: red - '--disabled-text-color': white type: 'custom:button-card' - action: service color: white icon: 'mdi:power' name: Test AC Raw service: action: ir_raw data: data: >- 3290, 1602, 424, 390, 424, 390, 424, 1232, 398, 390, 424, 1212, 420, 390, 424, 390, 424, 390, 424, 1232, 398, 1234, 398, 390, 424, 390, 426, 390, 424, 1232, 400, 1230, 398, 392, 424, 390, 426, 390, 426, 390, 424, 390, 424, 390, 424, 390, 424, 392, 424, 390, 424, 392, 424, 390, 424, 390, 424, 390, 424, 1232, 398, 390, 424, 390, 426, 390, 424, 390, 424, 392, 424, 390, 424, 392, 426, 1230, 400, 390, 424, 390, 426, 390, 424, 390, 424, 1232, 400, 1232, 398, 1232, 398, 1232, 400, 1232, 398, 1232, 400, 1232, 400, 1232, 400, 390, 426, 390, 424, 1206, 424, 390, 424, 390, 424, 392, 424, 390, 424, 392, 424, 390, 426, 390, 424, 390, 424, 1230, 402, 1230, 402, 390, 424, 390, 424, 1230, 402, 390, 424, 390, 424, 390, 424, 390, 424, 390, 426, 390, 424, 1230, 402, 1228, 402, 390, 424, 390, 424, 390, 426, 390, 424, 390, 426, 390, 424, 390, 424, 390, 426, 390, 426, 390, 424, 390, 424, 390, 426, 390, 424, 390, 424, 392, 426, 390, 424, 390, 424, 392, 424, 390, 424, 390, 424, 390, 424, 390, 424, 390, 424, 390, 424, 390, 424, 390, 426, 390, 426, 390, 424, 390, 424, 392, 424, 390, 424, 390, 424, 390, 424, 390, 424, 392, 424, 390, 424, 390, 424, 390, 426, 390, 424, 392, 424, 390, 424, 392, 424, 390, 424, 390, 424, 1228, 404, 388, 424, 390, 424, 392, 424, 1228, 404, 1228, 402, 1228, 402, 390, 426, 1228, 402, 390, 424, 390, 424 room: bedroom domain: script style: - color: white - background: blue - '--disabled-text-color': white type: 'custom:button-card' type: vertical-stack type: vertical-stack ``` More info about parts needed and discussion about it: [IN THIS HA COMMUNITY THREAD](https://community.home-assistant.io/t/tasmota-mqtt-irhvac-controler/162915/31) ================================================ FILE: SERVICES.md ================================================ # Services in Tasmota IRHVAC for HA v0.108+ and newer Supprort for setting econo, turbo, quiet, light, filters, clean, beep and sleep via newly added services In Tasmota IRHVAC for HA v0.108+ I've added 8 more services for controlling Air Conditioner's functions like these mentioned above. By adding this functionality, this doesnt mean, that your AC support it. Nor that Tasmota IRHVAC library supports it. You are using this functionality on your own will and risk. Newly added services are: ***tasmota_irhvac.set_econo*** with payload of: ```javacript {econo: "on", entity_id: clima.your_clima_entity_id} ``` where *econo* can be "on" or "off" and entity_id can be your climate entity_id, like, for example, *climate.kitchen_ac* ***tasmota_irhvac.set_turbo*** with payload of: ```javacript {turbo: "on", entity_id: clima.your_clima_entity_id} ``` where *turbo* can be "on" or "off" and entity_id can be your climate entity_id, like, for example, *climate.kitchen_ac* ***tasmota_irhvac.set_quiet*** with payload of: ```javacript {quiet: "on", entity_id: clima.your_clima_entity_id} ``` where *quiet* can be "on" or "off" and entity_id can be your climate entity_id, like, for example, *climate.kitchen_ac* ***tasmota_irhvac.set_light*** with payload of: ```javacript {light: "on", entity_id: clima.your_clima_entity_id} ``` where *light:* can be "on" or "off" and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac* ***tasmota_irhvac.set_filters*** with payload of: ```javacript {filters: "on", entity_id: clima.your_clima_entity_id} ``` where *filters:* can be "on" or "off" and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac* * Note that it is **filters** instead of **filter**, because "filter" is reserved word and we cannot use it.* ***tasmota_irhvac.set_clean*** with payload of: ```javacript {clean: "on", entity_id: clima.your_clima_entity_id} ``` where *clean:* can be "on" or "off" and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac* ***tasmota_irhvac.set_beep*** with payload of: ```javacript {beep: "on", entity_id: clima.your_clima_entity_id} ``` where *beep:* can be "on" or "off" and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac* ***tasmota_irhvac.set_sleep*** with payload of: ```javacript {sleep: "-1", entity_id: clima.your_clima_entity_id} ``` where *sleep:* can be any string, that your AC supports, and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac* # Example with Template Switch Example from **configuration.yaml**. Please, use only these services, that are supported from your AC! ```yaml switch: - platform: template switches: kitchen_climate_econo: friendly_name: "Econo" value_template: "{{ is_state_attr('climate.kitchen_ac', 'econo', 'on') }}" turn_on: service: tasmota_irhvac.set_econo data: entity_id: climate.kitchen_ac econo: 'on' turn_off: service: tasmota_irhvac.set_econo data: entity_id: climate.kitchen_ac econo: 'off' - platform: template switches: kitchen_climate_turbo: friendly_name: "Turbo" value_template: "{{ is_state_attr('climate.kitchen_ac', 'turbo', 'on') }}" turn_on: service: tasmota_irhvac.set_turbo data: entity_id: climate.kitchen_ac turbo: 'on' turn_off: service: tasmota_irhvac.set_turbo data: entity_id: climate.kitchen_ac turbo: 'off' - platform: template switches: kitchen_climate_quiet: friendly_name: "Quiet" value_template: "{{ is_state_attr('climate.kitchen_ac', 'quiet', 'on') }}" turn_on: service: tasmota_irhvac.set_quiet data: entity_id: climate.kitchen_ac quiet: 'on' turn_off: service: tasmota_irhvac.set_quiet data: entity_id: climate.kitchen_ac quiet: 'off' - platform: template switches: kitchen_climate_light: friendly_name: "Light" value_template: "{{ is_state_attr('climate.kitchen_ac', 'light', 'on') }}" turn_on: service: tasmota_irhvac.set_light data: entity_id: climate.kitchen_ac light: 'on' turn_off: service: tasmota_irhvac.set_light data: entity_id: climate.kitchen_ac light: 'off' - platform: template switches: kitchen_climate_filter: friendly_name: "Filter" value_template: "{{ is_state_attr('climate.kitchen_ac', 'filters', 'on') }}" turn_on: service: tasmota_irhvac.set_filters data: entity_id: climate.kitchen_ac filters: 'on' turn_off: service: tasmota_irhvac.set_filters data: entity_id: climate.kitchen_ac filters: 'off' - platform: template switches: kitchen_climate_clean: friendly_name: "Clean" value_template: "{{ is_state_attr('climate.kitchen_ac', 'clean', 'on') }}" turn_on: service: tasmota_irhvac.set_clean data: entity_id: climate.kitchen_ac clean: 'on' turn_off: service: tasmota_irhvac.set_clean data: entity_id: climate.kitchen_ac clean: 'off' - platform: template switches: kitchen_climate_beep: friendly_name: "Beep" value_template: "{{ is_state_attr('climate.kitchen_ac', 'beep', 'on') }}" turn_on: service: tasmota_irhvac.set_beep data: entity_id: climate.kitchen_ac beep: 'on' turn_off: service: tasmota_irhvac.set_beep data: entity_id: climate.kitchen_ac beep: 'off' - platform: template switches: kitchen_climate_sleep: friendly_name: "Sleep" value_template: "{{ is_state_attr('climate.kitchen_ac', 'sleep', '0') }}" turn_on: service: tasmota_irhvac.set_sleep data: entity_id: climate.kitchen_ac sleep: '1' turn_off: service: tasmota_irhvac.set_sleep data: entity_id: climate.kitchen_ac sleep: '0' ``` ================================================ FILE: blueprints/automation/tasmota_irhvac/climate_vane_control_tasmota-irhvac.yaml ================================================ blueprint: name: Climate Vane Control - Tasmota-IRHVAC description: Contoroling vertical or horizontal vane swing, potition of Climate on Tasmota-IRHVAC domain: automation source_url: https://github.com/hristo-atanasov/Tasmota-IRHVAC/blob/master/blueprints/automation/tasmota_irhvac/climate_vane_control_tasmota-irhvac.yaml input: climate_entity: name: Target Climate selector: entity: integration: tasmota_irhvac domain: climate input_entity: name: Dropdown Input Helper selector: entity: domain: input_select description: "The dropdown input helper entity can have the following options if the device supports it. ### Vertical Vane - off - auto - highest - high - low - lowest ### Horizontal Vane - off - auto - left max - left - middle - right - right max - wide " vane: name: Target Vane default: Vertical selector: select: options: - Vertical - Horizontal mode: parallel max: 2 variables: vane: attr: Vertical: swingv Horizontal: swingh srv: Vertical: tasmota_irhvac.set_swingv Horizontal: tasmota_irhvac.set_swingh key: !input vane climate_entity: !input climate_entity trigger: - platform: state entity_id: - !input climate_entity attribute: swingv id: Vertical - platform: state entity_id: - !input climate_entity attribute: swingh id: Horizontal - platform: state entity_id: - !input input_entity id: set action: - if: - condition: trigger id: set - condition: template value_template: "{{state_attr(climate_entity, vane.attr[key]) != states(trigger.entity_id)}}" then: - if: - condition: template value_template: "{{key == 'Vertical'}}" then: - service: "{{vane.srv[key]}}" data: entity_id: !input climate_entity swingv: "{{states(trigger.entity_id)}}" else: - service: "{{vane.srv[key]}}" data: entity_id: !input climate_entity swingh: "{{states(trigger.entity_id)}}" - if: - condition: template value_template: '{{key == trigger.id}}' then: - service: input_select.select_option data: option: "{{state_attr(climate_entity, vane.attr[key])}}" target: entity_id: !input input_entity ================================================ FILE: custom_components/tasmota_irhvac/__init__.py ================================================ """The Tasmota Irhvac climate component.""" ================================================ FILE: custom_components/tasmota_irhvac/climate.py ================================================ """Adds support for generic thermostat units.""" import asyncio import json import logging import uuid import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util import voluptuous as vol from homeassistant.components import mqtt try: from homeassistant.components.mqtt.schemas import MQTT_ENTITY_COMMON_SCHEMA except ImportError: from homeassistant.components.mqtt.mixins import MQTT_ENTITY_COMMON_SCHEMA from homeassistant.components.climate import PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA # try: # from homeassistant.components.climate import ClimateEntity # except ImportError: # from homeassistant.components.binary_sensor import ClimateDevice as ClimateEntity from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_SWING_MODE, FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, FAN_TOP, FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MIDDLE, FAN_OFF, FAN_ON, PRESET_AWAY, PRESET_NONE, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, ClimateEntityFeature, HVACAction, HVACMode, ) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_NAME, CONF_UNIQUE_ID, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfTemperature, ) from homeassistant.core import cached_property, callback from homeassistant.helpers import event as ha_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( ATTR_BEEP, ATTR_CLEAN, ATTR_ECONO, ATTR_FILTERS, ATTR_LAST_ON_MODE, ATTR_LIGHT, ATTR_QUIET, ATTR_SLEEP, ATTR_STATE_MODE, ATTR_SWINGH, ATTR_SWINGV, ATTR_TURBO, ATTRIBUTES_IRHVAC, CONF_AVAILABILITY_TOPIC, CONF_AWAY_TEMP, CONF_BEEP, CONF_CELSIUS, CONF_CLEAN, CONF_COMMAND_TOPIC, CONF_ECONO, CONF_EXCLUSIVE_GROUP_VENDOR, CONF_FAN_LIST, CONF_FILTER, CONF_HUMIDITY_SENSOR, CONF_IGNORE_OFF_TEMP, CONF_INITIAL_OPERATION_MODE, CONF_KEEP_MODE, CONF_LIGHT, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_MODEL, CONF_MODES_LIST, CONF_MQTT_DELAY, CONF_POWER_SENSOR, CONF_PRECISION, CONF_PROTOCOL, CONF_QUIET, CONF_SLEEP, CONF_SPECIAL_MODE, CONF_STATE_TOPIC, CONF_SWING_LIST, CONF_SWINGH, CONF_SWINGV, CONF_TARGET_TEMP, CONF_TEMP_SENSOR, CONF_TEMP_STEP, CONF_TOGGLE_LIST, CONF_TURBO, CONF_VENDOR, DATA_KEY, DEFAULT_COMMAND_TOPIC, DEFAULT_CONF_BEEP, DEFAULT_CONF_CELSIUS, DEFAULT_CONF_CLEAN, DEFAULT_CONF_ECONO, DEFAULT_CONF_FILTER, DEFAULT_CONF_KEEP_MODE, DEFAULT_CONF_LIGHT, DEFAULT_CONF_MODEL, DEFAULT_CONF_QUIET, DEFAULT_CONF_SLEEP, DEFAULT_CONF_TURBO, DEFAULT_FAN_LIST, DEFAULT_IGNORE_OFF_TEMP, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DEFAULT_MQTT_DELAY, DEFAULT_NAME, DEFAULT_PRECISION, DEFAULT_STATE_MODE, DEFAULT_STATE_TOPIC, DEFAULT_TARGET_TEMP, DOMAIN, HVAC_FAN_AUTO, HVAC_FAN_AUTO_MAX, HVAC_FAN_MAX, HVAC_FAN_MAX_HIGH, HVAC_FAN_MEDIUM, HVAC_FAN_MIN, HVAC_MODE_AUTO_FAN, HVAC_MODE_FAN_AUTO, HVAC_MODES, ON_OFF_LIST, SERVICE_BEEP_MODE, SERVICE_CLEAN_MODE, SERVICE_ECONO_MODE, SERVICE_FILTERS_MODE, SERVICE_LIGHT_MODE, SERVICE_QUIET_MODE, SERVICE_SET_SWINGH, SERVICE_SET_SWINGV, SERVICE_SLEEP_MODE, SERVICE_TURBO_MODE, STATE_AUTO, STATE_MODE_LIST, TOGGLE_ALL_LIST, ) DEFAULT_MODES_LIST = [ HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY, HVAC_MODE_AUTO_FAN, HVAC_MODE_FAN_AUTO, ] DEFAULT_SWING_LIST = [SWING_OFF, SWING_VERTICAL] DEFAULT_INITIAL_OPERATION_MODE = HVACMode.OFF _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE if hasattr(ClimateEntityFeature, "TURN_ON"): SUPPORT_FLAGS |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Exclusive(CONF_VENDOR, CONF_EXCLUSIVE_GROUP_VENDOR): cv.string, vol.Exclusive(CONF_PROTOCOL, CONF_EXCLUSIVE_GROUP_VENDOR): cv.string, vol.Required( CONF_COMMAND_TOPIC, default=DEFAULT_COMMAND_TOPIC ): mqtt.valid_publish_topic, vol.Optional(CONF_AVAILABILITY_TOPIC): mqtt.util.valid_topic, vol.Optional(CONF_TEMP_SENSOR): cv.entity_id, vol.Optional(CONF_HUMIDITY_SENSOR): cv.entity_id, vol.Optional(CONF_POWER_SENSOR): cv.entity_id, vol.Optional( CONF_STATE_TOPIC, default=DEFAULT_STATE_TOPIC ): mqtt.valid_subscribe_topic, vol.Optional(CONF_STATE_TOPIC + "_2"): mqtt.util.valid_topic, vol.Optional(CONF_MQTT_DELAY, default=DEFAULT_MQTT_DELAY): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP, default=DEFAULT_TARGET_TEMP): vol.Coerce(float), vol.Optional( CONF_INITIAL_OPERATION_MODE, default=DEFAULT_INITIAL_OPERATION_MODE ): vol.In(HVAC_MODES), vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float), vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.In( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), vol.Optional(CONF_TEMP_STEP, default=PRECISION_WHOLE): vol.In( [PRECISION_HALVES, PRECISION_WHOLE] ), vol.Optional(CONF_MODES_LIST, default=DEFAULT_MODES_LIST): vol.All( cv.ensure_list, [vol.In(HVAC_MODES)] ), vol.Optional(CONF_FAN_LIST, default=DEFAULT_FAN_LIST): vol.All( cv.ensure_list, [ vol.In( [ FAN_ON, FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_MIDDLE, FAN_FOCUS, FAN_DIFFUSE, FAN_TOP, HVAC_FAN_MIN, HVAC_FAN_MEDIUM, HVAC_FAN_MAX, HVAC_FAN_AUTO, HVAC_FAN_MAX_HIGH, HVAC_FAN_AUTO_MAX, ] ) ], ), vol.Optional(CONF_SWING_LIST, default=DEFAULT_SWING_LIST): vol.All( cv.ensure_list, [vol.In([SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL])], ), vol.Optional(CONF_QUIET, default=DEFAULT_CONF_QUIET): cv.string, vol.Optional(CONF_TURBO, default=DEFAULT_CONF_TURBO): cv.string, vol.Optional(CONF_ECONO, default=DEFAULT_CONF_ECONO): cv.string, vol.Optional(CONF_MODEL, default=DEFAULT_CONF_MODEL): cv.string, vol.Optional(CONF_CELSIUS, default=DEFAULT_CONF_CELSIUS): cv.string, vol.Optional(CONF_LIGHT, default=DEFAULT_CONF_LIGHT): cv.string, vol.Optional(CONF_FILTER, default=DEFAULT_CONF_FILTER): cv.string, vol.Optional(CONF_CLEAN, default=DEFAULT_CONF_CLEAN): cv.string, vol.Optional(CONF_BEEP, default=DEFAULT_CONF_BEEP): cv.string, vol.Optional(CONF_SLEEP, default=DEFAULT_CONF_SLEEP): cv.string, vol.Optional(CONF_KEEP_MODE, default=DEFAULT_CONF_KEEP_MODE): cv.boolean, vol.Optional(CONF_SWINGV): cv.string, vol.Optional(CONF_SWINGH): cv.string, vol.Optional(CONF_TOGGLE_LIST, default=[]): vol.All( cv.ensure_list, [vol.In(TOGGLE_ALL_LIST)], ), vol.Optional(CONF_IGNORE_OFF_TEMP, default=DEFAULT_IGNORE_OFF_TEMP): cv.boolean, vol.Optional(CONF_SPECIAL_MODE, default=""): cv.string, } ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(MQTT_ENTITY_COMMON_SCHEMA.schema) if hasattr(mqtt, "MQTT_BASE_PLATFORM_SCHEMA"): PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.MQTT_BASE_PLATFORM_SCHEMA.schema) else: PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.config.MQTT_BASE_SCHEMA.schema) IRHVAC_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_ECONO_MODE = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_ECONO): vol.In(ON_OFF_LIST), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_TURBO_MODE = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_TURBO): vol.In(ON_OFF_LIST), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_QUIET_MODE = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_QUIET): vol.In(ON_OFF_LIST), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_LIGHT_MODE = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_LIGHT): vol.In(ON_OFF_LIST), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_FILTERS_MODE = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_FILTERS): vol.In(ON_OFF_LIST), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_CLEAN_MODE = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_CLEAN): vol.In(ON_OFF_LIST), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_BEEP_MODE = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_BEEP): vol.In(ON_OFF_LIST), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_SLEEP_MODE = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_SLEEP): cv.string, vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_SET_SWINGV = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_SWINGV): vol.In( ["off", "auto", "highest", "high", "middle", "low", "lowest"] ), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_SCHEMA_SET_SWINGH = IRHVAC_SERVICE_SCHEMA.extend( { vol.Required(ATTR_SWINGH): vol.In( ["off", "auto", "left max", "left", "middle", "right", "right max", "wide"] ), vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In( STATE_MODE_LIST ), } ) SERVICE_TO_METHOD = { SERVICE_ECONO_MODE: { "method": "async_set_econo", "schema": SERVICE_SCHEMA_ECONO_MODE, }, SERVICE_TURBO_MODE: { "method": "async_set_turbo", "schema": SERVICE_SCHEMA_TURBO_MODE, }, SERVICE_QUIET_MODE: { "method": "async_set_quiet", "schema": SERVICE_SCHEMA_QUIET_MODE, }, SERVICE_LIGHT_MODE: { "method": "async_set_light", "schema": SERVICE_SCHEMA_LIGHT_MODE, }, SERVICE_FILTERS_MODE: { "method": "async_set_filters", "schema": SERVICE_SCHEMA_FILTERS_MODE, }, SERVICE_CLEAN_MODE: { "method": "async_set_clean", "schema": SERVICE_SCHEMA_CLEAN_MODE, }, SERVICE_BEEP_MODE: { "method": "async_set_beep", "schema": SERVICE_SCHEMA_BEEP_MODE, }, SERVICE_SLEEP_MODE: { "method": "async_set_sleep", "schema": SERVICE_SCHEMA_SLEEP_MODE, }, SERVICE_SET_SWINGV: { "method": "async_set_swingv", "schema": SERVICE_SCHEMA_SET_SWINGV, }, SERVICE_SET_SWINGH: { "method": "async_set_swingh", "schema": SERVICE_SCHEMA_SET_SWINGH, }, } async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the generic thermostat platform.""" vendor = config.get(CONF_VENDOR) protocol = config.get(CONF_PROTOCOL) name = config.get(CONF_NAME) if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} if vendor is None: if protocol is None: _LOGGER.error('Neither vendor nor protocol provided for "%s"!', name) return vendor = protocol tasmotaIrhvac = TasmotaIrhvac( hass, vendor, config, ) uuidstr = uuid.uuid4().hex hass.data[DATA_KEY][uuidstr] = tasmotaIrhvac async_add_entities([tasmotaIrhvac]) async def async_service_handler(service): """Map services to methods on TasmotaIrhvac.""" method = SERVICE_TO_METHOD.get(service.service, {}) params = { key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID } entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: devices = [ device for device in hass.data[DATA_KEY].values() if device.entity_id in entity_ids ] else: devices = hass.data[DATA_KEY].values() update_tasks = [] for device in devices: if not hasattr(device, method["method"]): continue await getattr(device, method["method"])(**params) update_tasks.append(asyncio.create_task(device.async_update_ha_state(True))) if update_tasks: await asyncio.wait(update_tasks) for irhvac_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[irhvac_service].get("schema", IRHVAC_SERVICE_SCHEMA) hass.services.async_register( DOMAIN, irhvac_service, async_service_handler, schema=schema ) class TasmotaIrhvac(RestoreEntity, ClimateEntity): """Representation of a Generic Thermostat device.""" # It can remove from HA >= 2025.1 # see https://developers.home-assistant.io/blog/2024/01/24/climate-climateentityfeatures-expanded/ _enable_turn_on_off_backwards_compatibility = False _last_on_mode: HVACMode | None def __init__( self, hass, vendor, config, ): """Initialize the thermostat.""" self.topic = config.get(CONF_COMMAND_TOPIC) self.hass = hass self._vendor = vendor self._temp_sensor = config.get(CONF_TEMP_SENSOR) self._humidity_sensor = config.get(CONF_HUMIDITY_SENSOR) self._power_sensor = config.get(CONF_POWER_SENSOR) self.state_topic = config[CONF_STATE_TOPIC] self.state_topic2 = config.get(CONF_STATE_TOPIC + "_2") self._away_temp = config.get(CONF_AWAY_TEMP) self._saved_target_temp = config[CONF_TARGET_TEMP] or self._away_temp self._temp_precision = config[CONF_PRECISION] self._enabled = False self.power_mode = None self._active = False self._mqtt_delay = config[CONF_MQTT_DELAY] self._min_temp = config[CONF_MIN_TEMP] self._max_temp = config[CONF_MAX_TEMP] self._def_target_temp = config[CONF_TARGET_TEMP] self._is_away = False self._modes_list = config[CONF_MODES_LIST] self._quiet = config[CONF_QUIET].lower() self._turbo = config[CONF_TURBO].lower() self._econo = config[CONF_ECONO].lower() self._model = config[CONF_MODEL] self._celsius = config[CONF_CELSIUS] self._light = config[CONF_LIGHT].lower() self._filter = config[CONF_FILTER].lower() self._clean = config[CONF_CLEAN].lower() self._beep = config[CONF_BEEP].lower() self._sleep = config[CONF_SLEEP].lower() self._sub_state = None self._keep_mode = config[CONF_KEEP_MODE] self._last_on_mode = None self._swingv = ( config.get(CONF_SWINGV).lower() if config.get(CONF_SWINGV) is not None else None ) self._swingh = ( config.get(CONF_SWINGH).lower() if config.get(CONF_SWINGH) is not None else None ) self._fix_swingv = None self._fix_swingh = None self._toggle_list = config[CONF_TOGGLE_LIST] self._state_mode = DEFAULT_STATE_MODE self._ignore_off_temp = config[CONF_IGNORE_OFF_TEMP] self._special_mode = config[CONF_SPECIAL_MODE] self._use_track_state_change_event = False self._unsubscribes = [] self.availability_topic = config.get(CONF_AVAILABILITY_TOPIC) if (self.availability_topic) is None: path = self.topic.split("/") self.availability_topic = "tele/" + path[1] + "/LWT" # Set _attr_* self._attr_unique_id = config.get(CONF_UNIQUE_ID) self._attr_name = config.get(CONF_NAME) self._attr_should_poll = False self._attr_temperature_unit = ( UnitOfTemperature.CELSIUS if self._celsius.lower() == "on" else UnitOfTemperature.FAHRENHEIT ) self._attr_hvac_mode = config.get(CONF_INITIAL_OPERATION_MODE) self._attr_target_temperature_step = config[CONF_TEMP_STEP] self._attr_hvac_modes = config[CONF_MODES_LIST] self.use_electra_tweak = False self._attr_fan_modes = config.get(CONF_FAN_LIST) if ( isinstance(self._attr_fan_modes, list) and HVAC_FAN_MAX_HIGH in self._attr_fan_modes and HVAC_FAN_AUTO_MAX in self._attr_fan_modes ): self.use_electra_tweak = True new_fan_list = [] for val in self._attr_fan_modes: if val == HVAC_FAN_MAX_HIGH: new_fan_list.append(FAN_HIGH) elif val == HVAC_FAN_AUTO_MAX: new_fan_list.append(HVAC_FAN_MAX) else: new_fan_list.append(val) self._attr_fan_modes = new_fan_list if len(new_fan_list) else None self._attr_fan_mode = ( self._attr_fan_modes[0] if isinstance(self._attr_fan_modes, list) and len(self._attr_fan_modes) else None ) self._attr_swing_modes = config.get(CONF_SWING_LIST) self._attr_swing_mode = ( self._attr_swing_modes[0] if isinstance(self._attr_swing_modes, list) and len(self._attr_swing_modes) else None ) self._attr_preset_modes = ( [PRESET_NONE, PRESET_AWAY] if self._away_temp else None ) self._attr_preset_mode = None self._attr_current_temperature = None self._attr_current_humidity = None self._attr_target_temperature = self._def_target_temp self._support_flags = SUPPORT_FLAGS if self._away_temp is not None: self._support_flags = self._support_flags | ClimateEntityFeature.PRESET_MODE if self._attr_swing_mode is not None: self._support_flags = self._support_flags | ClimateEntityFeature.SWING_MODE async def async_added_to_hass(self): # Replacing `async_track_state_change` with `async_track_state_change_event` # See, https://developers.home-assistant.io/blog/2024/04/13/deprecate_async_track_state_change/ if hasattr(ha_event, "async_track_state_change_event"): self._use_track_state_change_event = True def regist_track_state_change_event(entity_id): if self._use_track_state_change_event: ha_event.async_track_state_change_event( self.hass, entity_id, self._async_sensor_changed ) else: ha_event.async_track_state_change( self.hass, entity_id, self._async_sensor_changed ) # Make sure MQTT integration is enabled and the client is available await mqtt.async_wait_for_mqtt_client(self.hass) """Run when entity about to be added.""" await super().async_added_to_hass() # Add listener self._unsubscribes = await self._subscribe_topics() # Check If we have an old state old_state = await self.async_get_last_state() if old_state is not None: # If we have no initial temperature, restore if old_state.attributes.get(ATTR_TEMPERATURE) is not None: self._attr_target_temperature = TemperatureConverter.convert( float(old_state.attributes[ATTR_TEMPERATURE]), self.hass.config.units.temperature_unit, self.temperature_unit, ) if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY: self._is_away = True if old_state.attributes.get(ATTR_FAN_MODE) is not None: self._attr_fan_mode = old_state.attributes.get(ATTR_FAN_MODE) if old_state.attributes.get(ATTR_SWING_MODE) is not None: self._attr_swing_mode = old_state.attributes.get(ATTR_SWING_MODE) if old_state.attributes.get(ATTR_LAST_ON_MODE) is not None: self._last_on_mode = old_state.attributes.get(ATTR_LAST_ON_MODE) for attr, prop in ATTRIBUTES_IRHVAC.items(): val = old_state.attributes.get(attr) if val is not None: setattr(self, "_" + prop, val) if old_state.state: self._attr_hvac_mode = ( HVACMode.OFF if old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] else old_state.state ) self._enabled = self._attr_hvac_mode != HVACMode.OFF if self._enabled: self._last_on_mode = self._attr_hvac_mode if self._swingv != "auto": self._fix_swingv = self._swingv if self._swingh != "auto": self._fix_swingh = self._swingh # No previous target temperature, try and restore defaults if self._attr_target_temperature is None or self._attr_target_temperature < 1: self._attr_target_temperature = self._def_target_temp _LOGGER.warning( "No previously saved target temperature, setting to default value %s", self._attr_target_temperature, ) self.async_write_ha_state() if self._attr_hvac_mode == HVACMode.OFF: self.power_mode = STATE_OFF self._enabled = False else: self.power_mode = STATE_ON self._enabled = True for key in self._toggle_list: setattr(self, "_" + key.lower(), "off") if self._temp_sensor: regist_track_state_change_event(self._temp_sensor) temp_sensor_state = self.hass.states.get(self._temp_sensor) if ( temp_sensor_state and temp_sensor_state.state != STATE_UNKNOWN and temp_sensor_state.state != STATE_UNAVAILABLE ): self._async_update_temp(temp_sensor_state) if self._humidity_sensor: regist_track_state_change_event(self._humidity_sensor) humidity_sensor_state = self.hass.states.get(self._humidity_sensor) if ( humidity_sensor_state and humidity_sensor_state.state != STATE_UNKNOWN and humidity_sensor_state.state != STATE_UNAVAILABLE ): self._async_update_humidity(humidity_sensor_state) if self._power_sensor: regist_track_state_change_event(self._power_sensor) async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @callback async def available_message_received(message: mqtt.ReceiveMessage) -> None: msg = message.payload _LOGGER.debug(msg) if msg == "Online" or msg == "Offline": self._attr_available = True if msg == "Online" else False self.async_schedule_update_ha_state() @callback async def state_message_received(message: mqtt.ReceiveMessage) -> None: """Handle new MQTT state messages.""" try: json_payload = json.loads(message.payload) except ValueError: _LOGGER.error("Unable to parse MQTT payload as JSON: %s", message.payload) return _LOGGER.debug(json_payload) # If listening to `tele`, result looks like: {"IrReceived":{"Protocol":"XXX", ... ,"IRHVAC":{ ... }}} # we want to extract the data. if "IrReceived" in json_payload: json_payload = json_payload["IrReceived"] # By now the payload must include an `IRHVAC` field. if "IRHVAC" not in json_payload: return payload = json_payload["IRHVAC"] if payload["Vendor"] == self._vendor: # All values in the payload are Optional prev_power = self.power_mode if "Power" in payload: self.power_mode = payload["Power"].lower() if "Mode" in payload: self._attr_hvac_mode = payload["Mode"].lower() # Some vendors send/receive mode as fan instead of fan_only if self._attr_hvac_mode == HVACAction.FAN: self._attr_hvac_mode = HVACMode.FAN_ONLY if "Temp" in payload: if payload["Temp"] > 0: if not (self.power_mode == STATE_OFF and self._ignore_off_temp): self._attr_target_temperature = payload["Temp"] if "Celsius" in payload: self._celsius = payload["Celsius"].lower() if "Quiet" in payload: self._quiet = payload["Quiet"].lower() if "Turbo" in payload: self._turbo = payload["Turbo"].lower() if "Econo" in payload: self._econo = payload["Econo"].lower() if "Light" in payload: self._light = payload["Light"].lower() if "Filter" in payload: self._filter = payload["Filter"].lower() if "Clean" in payload: self._clean = payload["Clean"].lower() if "Beep" in payload: self._beep = payload["Beep"].lower() if "Sleep" in payload: self._sleep = payload["Sleep"] if "SwingV" in payload: self._swingv = payload["SwingV"].lower() if self._swingv != "auto": self._fix_swingv = self._swingv if "SwingH" in payload: self._swingh = payload["SwingH"].lower() if self._swingh != "auto": self._fix_swingh = self._swingh if ( "SwingV" in payload and payload["SwingV"].lower() == STATE_AUTO and "SwingH" in payload and payload["SwingH"].lower() == STATE_AUTO ): if SWING_BOTH in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_BOTH elif SWING_VERTICAL in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_VERTICAL elif SWING_HORIZONTAL in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_HORIZONTAL else: self._attr_swing_mode = SWING_OFF elif ( "SwingV" in payload and payload["SwingV"].lower() == STATE_AUTO and SWING_VERTICAL in (self._attr_swing_modes or []) ): self._attr_swing_mode = SWING_VERTICAL elif ( "SwingH" in payload and payload["SwingH"].lower() == STATE_AUTO and SWING_HORIZONTAL in (self._attr_swing_modes or []) ): self._attr_swing_mode = SWING_HORIZONTAL else: self._attr_swing_mode = SWING_OFF if "FanSpeed" in payload: fan_mode = payload["FanSpeed"].lower() # ELECTRA_AC fan modes fix if self.use_electra_tweak: if fan_mode == HVAC_FAN_MAX: self._attr_fan_mode = FAN_HIGH elif fan_mode == HVAC_FAN_AUTO: self._attr_fan_mode = HVAC_FAN_MAX elif fan_mode == HVAC_FAN_MIN: self._attr_fan_mode = FAN_LOW else: self._attr_fan_mode = fan_mode else: self._attr_fan_mode = fan_mode _LOGGER.debug(self._attr_fan_mode) if self._attr_hvac_mode != HVACMode.OFF: self._last_on_mode = self._attr_hvac_mode # Set default state to off if self.power_mode == STATE_OFF: self._attr_hvac_mode = HVACMode.OFF self._enabled = False else: self._enabled = True # Set toggles to 'off' for key in self._toggle_list: setattr(self, "_" + key.lower(), "off") # Update HA UI and State self.async_schedule_update_ha_state() # Check power sensor state if ( self._power_sensor and prev_power is not None and prev_power != self.power_mode ): await asyncio.sleep(3) state = self.hass.states.get(self._power_sensor) # It's probably running in a special mode, such as an automatic cleaning function. is_special_mode = ( True if state is not None and state.state else False ) await self._async_power_sensor_changed(None, state, is_special_mode) unsubscribe = [] unsubscribe.append( await mqtt.async_subscribe( self.hass, self.state_topic, state_message_received ) ) unsubscribe.append( await mqtt.async_subscribe( self.hass, self.availability_topic, available_message_received ) ) if self.state_topic2: unsubscribe.append( await mqtt.async_subscribe( self.hass, self.state_topic2, state_message_received ) ) return unsubscribe async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" for unsubscribe in self._unsubscribes: unsubscribe() @property def precision(self): """Return the precision of the system.""" if self._temp_precision is not None: return self._temp_precision return super().precision # This extension property is written throughout the instance, so use @property instead of @cached_property. @property def hvac_action(self): """Return the current running hvac operation if supported. Need to be one of CURRENT_HVAC_*. """ if self._attr_hvac_mode == HVACMode.OFF: return HVACAction.OFF elif self._attr_hvac_mode == HVACMode.HEAT: return HVACAction.HEATING elif self._attr_hvac_mode == HVACMode.COOL: return HVACAction.COOLING elif self._attr_hvac_mode == HVACMode.DRY: return HVACAction.DRYING elif self._attr_hvac_mode == HVACMode.FAN_ONLY: return HVACAction.FAN # This extension property is written throughout the instance, so use @property instead of @cached_property. @property def extra_state_attributes(self): """Return the state attributes of the device.""" return { attr: getattr(self, "_" + prop) for attr, prop in ATTRIBUTES_IRHVAC.items() } @property def last_on_mode(self): """Return the last non-idle mode ie. heat, cool.""" return self._last_on_mode async def async_set_hvac_mode(self, hvac_mode): """Set hvac mode.""" await self.set_mode(hvac_mode) # Ensure we update the current operation after changing the mode await self.async_send_cmd() async def async_turn_on(self): """Turn thermostat on.""" self._attr_hvac_mode = ( self._last_on_mode if self._last_on_mode is not None else HVACMode.AUTO ) self.power_mode = STATE_ON await self.async_send_cmd() async def async_turn_off(self): """Turn thermostat off.""" self._attr_hvac_mode = HVACMode.OFF self.power_mode = STATE_OFF await self.async_send_cmd() async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) hvac_mode = kwargs.get(ATTR_HVAC_MODE) if temperature is None: return if hvac_mode is not None: await self.set_mode(hvac_mode) self._attr_target_temperature = temperature if not self._attr_hvac_mode == HVACMode.OFF: self.power_mode = STATE_ON await self.async_send_cmd() async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" if fan_mode not in (self._attr_fan_modes or []): # tweak for some ELECTRA_AC devices if self.use_electra_tweak: if fan_mode != FAN_HIGH and fan_mode != HVAC_FAN_MAX: _LOGGER.error( "Invalid swing mode selected. Got '%s'. Allowed modes are:", fan_mode, ) _LOGGER.error(self._attr_fan_modes) return else: _LOGGER.error( "Invalid swing mode selected. Got '%s'. Allowed modes are:", fan_mode, ) _LOGGER.error(self._attr_fan_modes) return self._attr_fan_mode = fan_mode if not self._attr_hvac_mode == HVACMode.OFF: self.power_mode = STATE_ON await self.async_send_cmd() async def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" if swing_mode not in (self._attr_swing_modes or []): _LOGGER.error( "Invalid swing mode selected. Got '%s'. Allowed modes are:", swing_mode ) _LOGGER.error(self._attr_swing_modes) return self._attr_swing_mode = swing_mode # note: set _swingv and _swingh in send_ir() later if not self._attr_hvac_mode == HVACMode.OFF: self.power_mode = STATE_ON await self.async_send_cmd() async def async_set_econo(self, econo, state_mode): """Set new target econo mode.""" if econo not in ON_OFF_LIST: return self._econo = econo.lower() self._state_mode = state_mode await self.async_send_cmd() async def async_set_turbo(self, turbo, state_mode): """Set new target turbo mode.""" if turbo not in ON_OFF_LIST: return self._turbo = turbo.lower() self._state_mode = state_mode await self.async_send_cmd() async def async_set_quiet(self, quiet, state_mode): """Set new target quiet mode.""" if quiet not in ON_OFF_LIST: return self._quiet = quiet.lower() self._state_mode = state_mode await self.async_send_cmd() async def async_set_light(self, light, state_mode): """Set new target light mode.""" if light not in ON_OFF_LIST: return self._light = light.lower() self._state_mode = state_mode await self.async_send_cmd() async def async_set_filters(self, filters, state_mode): """Set new target filters mode.""" if filters not in ON_OFF_LIST: return self._filter = filters.lower() self._state_mode = state_mode await self.async_send_cmd() async def async_set_clean(self, clean, state_mode): """Set new target clean mode.""" if clean not in ON_OFF_LIST: return self._clean = clean.lower() self._state_mode = state_mode await self.async_send_cmd() async def async_set_beep(self, beep, state_mode): """Set new target beep mode.""" if beep not in ON_OFF_LIST: return self._beep = beep.lower() self._state_mode = state_mode await self.async_send_cmd() async def async_set_sleep(self, sleep, state_mode): """Set new target sleep mode.""" self._sleep = sleep.lower() self._state_mode = state_mode await self.async_send_cmd() async def async_set_swingv(self, swingv, state_mode): """Set new target swingv.""" self._swingv = swingv.lower() if self._swingv != "auto": self._fix_swingv = self._swingv if self._attr_swing_mode == SWING_BOTH: if SWING_HORIZONTAL in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_HORIZONTAL elif self._attr_swing_mode == SWING_VERTICAL: self._attr_swing_mode = SWING_OFF else: if self._attr_swing_mode == SWING_HORIZONTAL: if SWING_BOTH in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_BOTH else: if SWING_VERTICAL in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_VERTICAL self._state_mode = state_mode await self.async_send_cmd() async def async_set_swingh(self, swingh, state_mode): """Set new target swingh.""" self._swingh = swingh.lower() if self._swingh != "auto": self._fix_swingh = self._swingh if self._attr_swing_mode == SWING_BOTH: if SWING_VERTICAL in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_VERTICAL elif self._attr_swing_mode == SWING_HORIZONTAL: self._attr_swing_mode = SWING_OFF else: if self._attr_swing_mode == SWING_VERTICAL: if SWING_BOTH in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_BOTH else: if SWING_HORIZONTAL in (self._attr_swing_modes or []): self._attr_swing_mode = SWING_HORIZONTAL self._state_mode = state_mode await self.async_send_cmd() async def async_send_cmd(self): await self.send_ir() @cached_property def min_temp(self): """Return the minimum temperature.""" if self._min_temp: return self._min_temp # get default temp from super class return super().min_temp @cached_property def max_temp(self): """Return the maximum temperature.""" if self._max_temp: return self._max_temp # Get default temp from super class return super().max_temp async def _async_sensor_changed( self, entity_id_or_event, old_state=None, new_state=None ): # Replacing `async_track_state_change` with `async_track_state_change_event` # See, https://developers.home-assistant.io/blog/2024/04/13/deprecate_async_track_state_change/ if self._use_track_state_change_event: entity_id = entity_id_or_event.data["entity_id"] old_state = entity_id_or_event.data["old_state"] new_state = entity_id_or_event.data["new_state"] else: entity_id = entity_id_or_event if new_state is None: return if entity_id == self._temp_sensor: self._async_update_temp(new_state) self.async_schedule_update_ha_state() elif entity_id == self._humidity_sensor: self._async_update_humidity(new_state) self.async_schedule_update_ha_state() elif entity_id == self._power_sensor: await self._async_power_sensor_changed(old_state, new_state) async def _async_power_sensor_changed( self, old_state, new_state, is_special_mode=False ): """Handle power sensor changes.""" if new_state is None: return if old_state is not None and new_state.state == old_state.state: return if new_state.state == STATE_ON: if self._attr_hvac_mode == HVACMode.OFF or self.power_mode == STATE_OFF: self._attr_hvac_mode = ( self._special_mode if self._special_mode and is_special_mode else self._last_on_mode ) self.power_mode = STATE_ON self.async_schedule_update_ha_state() elif new_state.state == STATE_OFF: if self._attr_hvac_mode != HVACMode.OFF or self.power_mode == STATE_ON: self._attr_hvac_mode = HVACMode.OFF self.power_mode = STATE_OFF self.async_schedule_update_ha_state() @callback def _async_update_temp(self, state): """Update thermostat with latest state from sensor.""" if state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): return try: self._attr_current_temperature = TemperatureConverter.convert( float(state.state), state.attributes["unit_of_measurement"], self.temperature_unit, ) except (ValueError, KeyError) as ex: _LOGGER.error("Unable to update from sensor: %s", ex) @callback def _async_update_humidity(self, state): """Update thermostat with latest state from humidity sensor.""" try: if state.state != STATE_UNKNOWN and state.state != STATE_UNAVAILABLE: self._attr_current_humidity = int(float(state.state)) except ValueError as ex: _LOGGER.error("Unable to update from humidity sensor: %s", ex) @cached_property def supported_features(self): """Return the list of supported features.""" return self._support_flags async def async_set_preset_mode(self, preset_mode): """Set new preset mode. This method must be run in the event loop and returns a coroutine. """ if preset_mode == PRESET_AWAY and not self._is_away: self._is_away = True self._saved_target_temp = self._attr_target_temperature self._attr_target_temperature = self._away_temp elif preset_mode == PRESET_NONE and self._is_away: self._is_away = False self._attr_target_temperature = self._saved_target_temp self._attr_preset_mode = PRESET_AWAY if self._is_away else PRESET_NONE await self.send_ir() async def set_mode(self, hvac_mode): """Set hvac mode.""" hvac_mode = hvac_mode.lower() if hvac_mode not in self._attr_hvac_modes or hvac_mode == HVACMode.OFF: self._attr_hvac_mode = HVACMode.OFF self._enabled = False self.power_mode = STATE_OFF else: self._attr_hvac_mode = self._last_on_mode = hvac_mode self._enabled = True self.power_mode = STATE_ON async def send_ir(self): """Send the payload to tasmota mqtt topic.""" fan_speed = self.fan_mode # tweak for some ELECTRA_AC devices if self.use_electra_tweak: if self.fan_mode == FAN_HIGH: fan_speed = HVAC_FAN_MAX if self.fan_mode == HVAC_FAN_MAX: fan_speed = HVAC_FAN_AUTO if self.fan_mode == FAN_LOW: fan_speed = HVAC_FAN_MIN # Set the swing mode - default off self._swingv = STATE_OFF if self._fix_swingv is None else self._fix_swingv self._swingh = STATE_OFF if self._fix_swingh is None else self._fix_swingh if SWING_BOTH in (self._attr_swing_modes or []) or SWING_VERTICAL in ( self._attr_swing_modes or [] ): if ( self._attr_swing_mode == SWING_BOTH or self._attr_swing_mode == SWING_VERTICAL ): self._swingv = STATE_AUTO if SWING_BOTH in (self._attr_swing_modes or []) or SWING_HORIZONTAL in ( self._attr_swing_modes or [] ): if ( self._attr_swing_mode == SWING_BOTH or self._attr_swing_mode == SWING_HORIZONTAL ): self._swingh = STATE_AUTO _dt = dt_util.now() _min = _dt.hour * 60 + _dt.minute # Populate the payload payload_data = { "StateMode": self._state_mode, "Vendor": self._vendor, "Model": self._model, "Power": self.power_mode, "Mode": self._last_on_mode if self._keep_mode else self._attr_hvac_mode, "Celsius": self._celsius, "Temp": round(self._attr_target_temperature / self._temp_precision) * self._temp_precision, "FanSpeed": fan_speed, "SwingV": self._swingv, "SwingH": self._swingh, "Quiet": self._quiet, "Turbo": self._turbo, "Econo": self._econo, "Light": self._light, "Filter": self._filter, "Clean": self._clean, "Beep": self._beep, "Sleep": self._sleep, "Clock": int(_min), "Weekday": int(_dt.weekday()), } self._state_mode = DEFAULT_STATE_MODE for key in self._toggle_list: setattr(self, "_" + key.lower(), "off") payload = json.dumps(payload_data) # Publish mqtt message if float(self._mqtt_delay) != float(DEFAULT_MQTT_DELAY): await asyncio.sleep(float(self._mqtt_delay)) await mqtt.async_publish(self.hass, self.topic, payload) # Update HA UI and State self.async_schedule_update_ha_state() ================================================ FILE: custom_components/tasmota_irhvac/const.py ================================================ """Provides the constants needed for component.""" from homeassistant.components.climate.const import HVACMode # States STATE_AUTO = "auto" STATE_COOL = "cool" STATE_DRY = "dry" STATE_FAN_ONLY = "fan_only" STATE_HEAT = "heat" # Fan speeds HVAC_FAN_AUTO = "auto" HVAC_FAN_MIN = "min" HVAC_FAN_MEDIUM = "medium" HVAC_FAN_MAX = "max" # Some devices have "auto" and "fan_only" changed HVAC_MODE_AUTO_FAN = "auto_fan_only" # Some devicec have "fan_only" and "auto" changed HVAC_MODE_FAN_AUTO = "fan_only_auto" # Some devices say max,but it is high, and auto which is max HVAC_FAN_MAX_HIGH = "max_high" HVAC_FAN_AUTO_MAX = "auto_max" # Hvac moed list HVAC_MODES = [ HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL, HVACMode.AUTO, HVACMode.DRY, HVACMode.FAN_ONLY, HVAC_MODE_AUTO_FAN, HVAC_MODE_FAN_AUTO, ] # Platform specific config entry names CONF_EXCLUSIVE_GROUP_VENDOR = "exclusive_group_vendor" CONF_VENDOR = "vendor" CONF_PROTOCOL = "protocol" # Soon to be deprecated CONF_COMMAND_TOPIC = "command_topic" CONF_STATE_TOPIC = "state_topic" CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_TEMP_SENSOR = "temperature_sensor" CONF_HUMIDITY_SENSOR = "humidity_sensor" CONF_POWER_SENSOR = "power_sensor" CONF_MQTT_DELAY = "mqtt_delay" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" CONF_TARGET_TEMP = "target_temp" CONF_INITIAL_OPERATION_MODE = "initial_operation_mode" CONF_AWAY_TEMP = "away_temp" CONF_PRECISION = "precision" CONF_TEMP_STEP = "temp_step" CONF_MODES_LIST = "supported_modes" CONF_FAN_LIST = "supported_fan_speeds" CONF_SWING_LIST = "supported_swing_list" CONF_QUIET = "default_quiet_mode" CONF_TURBO = "default_turbo_mode" CONF_ECONO = "default_econo_mode" CONF_MODEL = "hvac_model" CONF_CELSIUS = "celsius_mode" CONF_LIGHT = "default_light_mode" CONF_FILTER = "default_filter_mode" CONF_CLEAN = "default_clean_mode" CONF_BEEP = "default_beep_mode" CONF_SLEEP = "default_sleep_mode" CONF_KEEP_MODE = "keep_mode_when_off" CONF_SWINGV = "default_swingv" CONF_SWINGH = "default_swingh" CONF_TOGGLE_LIST = "toggle_list" CONF_IGNORE_OFF_TEMP = "ignore_off_temp" CONF_SPECIAL_MODE = "special_mode" # Platform specific default values DEFAULT_NAME = "IR AirConditioner" DEFAULT_STATE_TOPIC = "state" DEFAULT_COMMAND_TOPIC = "topic" DEFAULT_MQTT_DELAY = 0 DEFAULT_TARGET_TEMP = 26 DEFAULT_MIN_TEMP = 16 DEFAULT_MAX_TEMP = 32 DEFAULT_PRECISION = 1 DEFAULT_FAN_LIST = [HVAC_FAN_AUTO_MAX, HVAC_FAN_MAX_HIGH, HVAC_FAN_MEDIUM, HVAC_FAN_MIN] DEFAULT_CONF_QUIET = "off" DEFAULT_CONF_TURBO = "off" DEFAULT_CONF_ECONO = "off" DEFAULT_CONF_MODEL = "-1" DEFAULT_CONF_CELSIUS = "on" DEFAULT_CONF_LIGHT = "off" DEFAULT_CONF_FILTER = "off" DEFAULT_CONF_CLEAN = "off" DEFAULT_CONF_BEEP = "off" DEFAULT_CONF_SLEEP = "-1" DEFAULT_CONF_KEEP_MODE = False DEFAULT_STATE_MODE = "SendStore" DEFAULT_IGNORE_OFF_TEMP = False ATTR_NAME = "name" ATTR_VALUE = "value" DATA_KEY = "tasmota_irhvac.climate" DOMAIN = "tasmota_irhvac" ATTR_ECONO = "econo" ATTR_TURBO = "turbo" ATTR_QUIET = "quiet" ATTR_LIGHT = "light" ATTR_FILTERS = "filters" ATTR_CLEAN = "clean" ATTR_BEEP = "beep" ATTR_SLEEP = "sleep" ATTR_LAST_ON_MODE = "last_on_mode" ATTR_SWINGV = "swingv" ATTR_SWINGH = "swingh" ATTR_FIX_SWINGV = "fix_swingv" ATTR_FIX_SWINGH = "fix_swingh" ATTR_STATE_MODE = "state_mode" SERVICE_ECONO_MODE = "set_econo" SERVICE_TURBO_MODE = "set_turbo" SERVICE_QUIET_MODE = "set_quiet" SERVICE_LIGHT_MODE = "set_light" SERVICE_FILTERS_MODE = "set_filters" SERVICE_CLEAN_MODE = "set_clean" SERVICE_BEEP_MODE = "set_beep" SERVICE_SLEEP_MODE = "set_sleep" SERVICE_SET_SWINGV = "set_swingv" SERVICE_SET_SWINGH = "set_swingh" # Map attributes to properties of the state object ATTRIBUTES_IRHVAC = { ATTR_ECONO: "econo", ATTR_TURBO: "turbo", ATTR_QUIET: "quiet", ATTR_LIGHT: "light", ATTR_FILTERS: "filter", ATTR_CLEAN: "clean", ATTR_BEEP: "beep", ATTR_SLEEP: "sleep", ATTR_LAST_ON_MODE: "last_on_mode", ATTR_SWINGV: "swingv", ATTR_SWINGH: "swingh", ATTR_FIX_SWINGV: "fix_swingv", ATTR_FIX_SWINGH: "fix_swingh", } ON_OFF_LIST = ["ON", "OFF", "On", "Off", "on", "off"] TOGGLE_ALL_LIST = [ "SwingV", "SwingH", "Quiet", "Turbo", "Econo", "Light", "Filter", "Clean", "Beep", "Sleep", ] STATE_MODE_LIST = ["StoreOnly", "SendStore"] ================================================ FILE: custom_components/tasmota_irhvac/manifest.json ================================================ { "domain": "tasmota_irhvac", "name": "Tasmota Irhvac", "version": "2026.4.5", "documentation": "https://github.com/hristo-atanasov/Tasmota-IRHVAC", "issue_tracker": "https://github.com/hristo-atanasov/Tasmota-IRHVAC/issues", "homeassistant": "2024.11.0", "requirements": [], "dependencies": [ "mqtt", "sensor" ], "codeowners": [ "@hristo-atanasov", "@nao-pon" ] } ================================================ FILE: custom_components/tasmota_irhvac/services.yaml ================================================ set_econo: description: Sets Econo mode. target: entity: integration: tasmota_irhvac fields: econo: description: Sets Econo mode example: "on" required: true selector: select: options: - "off" - "on" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_turbo: description: Sets Turbo mode. target: entity: integration: tasmota_irhvac fields: turbo: description: Sets Turbo mode example: "on" required: true selector: select: options: - "off" - "on" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_filters: description: Sets Filters mode. target: entity: integration: tasmota_irhvac fields: filters: description: Sets Filters mode example: "on" required: true selector: select: options: - "off" - "on" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_light: target: entity: integration: tasmota_irhvac description: Sets Light mode. fields: light: description: Sets Light mode example: "on" required: true selector: select: options: - "off" - "on" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_quiet: target: entity: integration: tasmota_irhvac description: Sets Quiet mode. fields: quiet: description: Sets Quiet mode example: "on" required: true selector: select: options: - "off" - "on" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_clean: target: entity: integration: tasmota_irhvac description: Sets Clean mode. fields: clean: description: Sets Clean mode example: "on" required: true selector: select: options: - "off" - "on" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_beep: target: entity: integration: tasmota_irhvac description: Sets Beep mode. fields: beep: description: Sets Beep mode example: "on" required: true selector: select: options: - "off" - "on" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_sleep: description: Sets Sleep mode. target: entity: integration: tasmota_irhvac fields: sleep: description: Sets Sleep mode example: "0" required: true state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_swingv: description: Sets vane vertical position. target: entity: integration: tasmota_irhvac fields: swingv: description: '"off", "auto", "highest", "high", "middle", "low" or "lowest", but only those supported by this model.' example: '"middle"' required: true selector: select: options: - "off" - "auto" - "highest" - "high" - "middle" - "low" - "lowest" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore set_swingh: name: Set swingh description: Sets vane horizonal position. target: entity: integration: tasmota_irhvac fields: swingh: description: '"off", "auto", "left max", "left", "middle", "right", "right max" or "wide", but only those supported by this model.' example: '"middle"' required: true selector: select: options: - "off" - "auto" - "left max" - "left" - "middle" - "right" - "right max" - "wide" state_mode: description: Sets StateMode in MQTT message. Default is "SendStore". example: "StoreOnly" required: false selector: select: options: - StoreOnly - SendStore ================================================ FILE: examples/card_configuration.yaml ================================================ cards: - cards: - action: service color: white icon: "mdi:power" name: Turn On Audio HEX service: action: ir_code data: bits: 12 data: A80 protocol: SONY room: kitchen domain: script style: - color: white - background: green - "--disabled-text-color": white type: "custom:button-card" - action: service color: white icon: "mdi:power" name: Turn Off Audio HEX service: action: ir_code data: bits: 12 data: E85 protocol: SONY room: kitchen domain: script style: - color: white - background: red - "--disabled-text-color": white type: "custom:button-card" - action: service color: white icon: "mdi:power" name: Test AC Raw service: action: ir_raw data: data: >- 3290, 1602, 424, 390, 424, 390, 424, 1232, 398, 390, 424, 1212, 420, 390, 424, 390, 424, 390, 424, 1232, 398, 1234, 398, 390, 424, 390, 426, 390, 424, 1232, 400, 1230, 398, 392, 424, 390, 426, 390, 426, 390, 424, 390, 424, 390, 424, 390, 424, 392, 424, 390, 424, 392, 424, 390, 424, 390, 424, 390, 424, 1232, 398, 390, 424, 390, 426, 390, 424, 390, 424, 392, 424, 390, 424, 392, 426, 1230, 400, 390, 424, 390, 426, 390, 424, 390, 424, 1232, 400, 1232, 398, 1232, 398, 1232, 400, 1232, 398, 1232, 400, 1232, 400, 1232, 400, 390, 426, 390, 424, 1206, 424, 390, 424, 390, 424, 392, 424, 390, 424, 392, 424, 390, 426, 390, 424, 390, 424, 1230, 402, 1230, 402, 390, 424, 390, 424, 1230, 402, 390, 424, 390, 424, 390, 424, 390, 424, 390, 426, 390, 424, 1230, 402, 1228, 402, 390, 424, 390, 424, 390, 426, 390, 424, 390, 426, 390, 424, 390, 424, 390, 426, 390, 426, 390, 424, 390, 424, 390, 426, 390, 424, 390, 424, 392, 426, 390, 424, 390, 424, 392, 424, 390, 424, 390, 424, 390, 424, 390, 424, 390, 424, 390, 424, 390, 424, 390, 426, 390, 426, 390, 424, 390, 424, 392, 424, 390, 424, 390, 424, 390, 424, 390, 424, 392, 424, 390, 424, 390, 424, 390, 426, 390, 424, 392, 424, 390, 424, 392, 424, 390, 424, 390, 424, 1228, 404, 388, 424, 390, 424, 392, 424, 1228, 404, 1228, 402, 1228, 402, 390, 426, 1228, 402, 390, 424, 390, 424 room: bedroom domain: script style: - color: white - background: blue - "--disabled-text-color": white type: "custom:button-card" type: vertical-stack type: vertical-stack ================================================ FILE: examples/configuration.yaml ================================================ climate: - platform: tasmota_irhvac name: "Some Name Here" command_topic: "cmnd/your_tasmota_device/irhvac" # Pick one of the following: # State is updated when the tasmota device receives an IR signal (includes own transmission and original remote) # useful when a normal remote is in use alongside the tasmota device, may be less reliable than the second option. state_topic: "tele/your_tasmota_device/RESULT" # State is updated when the tasmota device completes IR transmissionm, should be pretty reliable. #state_topic: "stat/your_tasmota_device/RESULT" # Optional second state topic, This option allows you to subscribe to both "tele" and "stat" messages. state_topic_2: "stat/your_tasmota_device/RESULT" # Uncomment if your 'available topic' of Tasmota IR device are different (if device in HA is disabled) #availability_topic: "tele/your_tasmota_device/LWT" temperature_sensor: sensor.kitchen_temperature humidity_sensor: sensor.kitchen_humidity #optional - default None power_sensor: binaly_sensor.kitchen_ac_power #optional - default None vendor: "ELECTRA_AC" # When operating grouped devices at the same time, MQTT commands are intentionally delayed to prevent multiple devices # from performing the same operation at the same time. This allows the high current peaks to be shifted. mqtt_delay: 0.0 #optional - default 0 int or 0.0 float value in [sec]. min_temp: 16 #optional - default 16 int value max_temp: 32 #optional - default 32 int value target_temp: 26 #optional - default 26 int value initial_operation_mode: "off" # optional - default "off" string value (one of the "supported_modes") away_temp: 24 #optional - default 24 int value precision: 1 #optional - default 1 int or float value. Can be set to 1, 0.5 or 0.1 supported_modes: - "heat" - "cool" - "dry" - "fan_only" # Use "fan_only" even if Tasmota shows "Mode":"Fan" - "auto" - "off" #Turns the AC off - Should be in quotes # Some devices have "auto" and "fan_only" switched # If the following two lines are uncommented, "auto" and "fan" shoud be commented out #- "auto_fan_only" #if remote shows fan but tasmota says auto #- "fan_only_auto" #if remote shows auto but tasmota says fan supported_fan_speeds: # Some devices say max,but it is high, and auto which is max # If you uncomment the following two, you have to comment high and max # - "auto_max" #woud become max # - "max_high" #would become high #- "on" #- "off" #- "low" - "medium" - "high" #- "middle" #- "focus" #- "diffuse" #- "top" - "min" - "max" #- "auto" supported_swing_list: - "off" - "vertical" #up to down # - "horizontal" # Left to right # - "both" default_quiet_mode: "Off" #optional - default "Off" string value default_turbo_mode: "Off" #optional - default "Off" string value default_econo_mode: "Off" #optional - default "Off" string value hvac_model: "-1" #optional - default "1" string value celsius_mode: "On" #optional - default "On" string value default_light_mode: "Off" #optional - default "Off" string value default_filter_mode: "Off" #optional - default "Off" string value default_clean_mode: "Off" #optional - default "Off" string value default_beep_mode: "Off" #optional - default "Off" string value default_sleep_mode: "-1" #optional - default "-1" string value default_swingv: "high" #optional - default "" string value default_swingh: "left" #optional - default "" string value keep_mode_when_off: True #optional - default False boolean value : Must be True for MITSUBISHI_AC, ECOCLIM, etc. # toggle_list: #optional - default [] # The toggled property is a setting that does not retain the On state. # Set this if your AC properties are toggle function. #- Beep #- Clean #- Econo #- Filter #- Light #- Quiet #- Sleep #- SwingH #- SwingV #- Turbo # When turning off some devices with their remote control they are set to the lowest temperature # and this is shown on the thermostat card. Setting `ignore_off_temp` value to True will keep the last target temperature displayed on the card. ignore_off_temp: False #optional - default False boolean value # Some air conditioners have a function to enter special modes such as cleaning # mode after operation. This mode detects and sets this. # "auto", "cool", "dry", "fan_only", "heat" or "off" special_mode: "" #optional - default "" is current mode string value ================================================ FILE: examples/scripts.yaml ================================================ ir_code: sequence: - data_template: payload: '{"Protocol":"{{ protocol }}","Bits": {{ bits }},"Data": 0x{{ data }}}' topic: "cmnd/{{ room }}Multisensor/irsend" service: mqtt.publish ir_raw: sequence: - data_template: payload: "0, {{ data }}" topic: "cmnd/{{ room }}Multisensor/irsend" service: mqtt.publish ================================================ FILE: hacs.json ================================================ { "name": "Tasmota-IRHVAC", "render_readme": true, "homeassistant": "2024.11.0" }