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
================================================
[](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*.

Tasmota configuration looks like this:

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).

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"
}
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
SYMBOL INDEX (37 symbols across 1 files)
FILE: custom_components/tasmota_irhvac/climate.py
function async_setup_platform (line 408) | async def async_setup_platform(hass, config, async_add_entities, discove...
class TasmotaIrhvac (line 467) | class TasmotaIrhvac(RestoreEntity, ClimateEntity):
method __init__ (line 476) | def __init__(
method async_added_to_hass (line 594) | async def async_added_to_hass(self):
method _subscribe_topics (line 700) | async def _subscribe_topics(self):
method async_will_remove_from_hass (line 868) | async def async_will_remove_from_hass(self):
method precision (line 874) | def precision(self):
method hvac_action (line 882) | def hvac_action(self):
method extra_state_attributes (line 900) | def extra_state_attributes(self):
method last_on_mode (line 907) | def last_on_mode(self):
method async_set_hvac_mode (line 911) | async def async_set_hvac_mode(self, hvac_mode):
method async_turn_on (line 917) | async def async_turn_on(self):
method async_turn_off (line 925) | async def async_turn_off(self):
method async_set_temperature (line 931) | async def async_set_temperature(self, **kwargs):
method async_set_fan_mode (line 946) | async def async_set_fan_mode(self, fan_mode):
method async_set_swing_mode (line 971) | async def async_set_swing_mode(self, swing_mode):
method async_set_econo (line 985) | async def async_set_econo(self, econo, state_mode):
method async_set_turbo (line 993) | async def async_set_turbo(self, turbo, state_mode):
method async_set_quiet (line 1001) | async def async_set_quiet(self, quiet, state_mode):
method async_set_light (line 1009) | async def async_set_light(self, light, state_mode):
method async_set_filters (line 1017) | async def async_set_filters(self, filters, state_mode):
method async_set_clean (line 1025) | async def async_set_clean(self, clean, state_mode):
method async_set_beep (line 1033) | async def async_set_beep(self, beep, state_mode):
method async_set_sleep (line 1041) | async def async_set_sleep(self, sleep, state_mode):
method async_set_swingv (line 1047) | async def async_set_swingv(self, swingv, state_mode):
method async_set_swingh (line 1067) | async def async_set_swingh(self, swingh, state_mode):
method async_send_cmd (line 1087) | async def async_send_cmd(self):
method min_temp (line 1091) | def min_temp(self):
method max_temp (line 1100) | def max_temp(self):
method _async_sensor_changed (line 1108) | async def _async_sensor_changed(
method _async_power_sensor_changed (line 1132) | async def _async_power_sensor_changed(
method _async_update_temp (line 1159) | def _async_update_temp(self, state):
method _async_update_humidity (line 1173) | def _async_update_humidity(self, state):
method supported_features (line 1182) | def supported_features(self):
method async_set_preset_mode (line 1186) | async def async_set_preset_mode(self, preset_mode):
method set_mode (line 1201) | async def set_mode(self, hvac_mode):
method send_ir (line 1213) | async def send_ir(self):
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (88K chars).
[
{
"path": ".github/workflows/validate.yml",
"chars": 311,
"preview": "name: Validate\n\non:\n push:\n pull_request:\n schedule:\n - cron: \"0 0 * * *\"\n workflow_dispatch:\n\njobs:\n validate-h"
},
{
"path": "INFO.md",
"chars": 125,
"preview": "Tasmota-IRHVAC\nHome Assistant platform for controlling IR Air Conditioners via Tasmota IRHVAC command and compatible har"
},
{
"path": "README.md",
"chars": 7831,
"preview": "[](https://github.com/custom-compo"
},
{
"path": "SERVICES.md",
"chars": 6462,
"preview": "# Services in Tasmota IRHVAC for HA v0.108+ and newer\nSupprort for setting econo, turbo, quiet, light, filters, clean, b"
},
{
"path": "blueprints/automation/tasmota_irhvac/climate_vane_control_tasmota-irhvac.yaml",
"chars": 2507,
"preview": "blueprint:\n name: Climate Vane Control - Tasmota-IRHVAC\n description: Contoroling vertical or horizontal vane swing, p"
},
{
"path": "custom_components/tasmota_irhvac/__init__.py",
"chars": 44,
"preview": "\"\"\"The Tasmota Irhvac climate component.\"\"\"\n"
},
{
"path": "custom_components/tasmota_irhvac/climate.py",
"chars": 47407,
"preview": "\"\"\"Adds support for generic thermostat units.\"\"\"\n\nimport asyncio\nimport json\nimport logging\nimport uuid\n\nimport homeassi"
},
{
"path": "custom_components/tasmota_irhvac/const.py",
"chars": 4354,
"preview": "\"\"\"Provides the constants needed for component.\"\"\"\n\nfrom homeassistant.components.climate.const import HVACMode\n\n# State"
},
{
"path": "custom_components/tasmota_irhvac/manifest.json",
"chars": 403,
"preview": "{\n \"domain\": \"tasmota_irhvac\",\n \"name\": \"Tasmota Irhvac\",\n \"version\": \"2026.4.5\",\n \"documentation\": \"https://github."
},
{
"path": "custom_components/tasmota_irhvac/services.yaml",
"chars": 5677,
"preview": "set_econo:\n description: Sets Econo mode.\n target:\n entity:\n integration: tasmota_irhvac\n fields:\n econo:\n"
},
{
"path": "examples/card_configuration.yaml",
"chars": 3073,
"preview": "cards:\n - cards:\n - action: service\n color: white\n icon: \"mdi:power\"\n name: Turn On Audio HEX"
},
{
"path": "examples/configuration.yaml",
"chars": 4728,
"preview": "climate:\n - platform: tasmota_irhvac\n name: \"Some Name Here\"\n command_topic: \"cmnd/your_tasmota_device/irhvac\"\n "
},
{
"path": "examples/scripts.yaml",
"chars": 363,
"preview": "ir_code:\n sequence:\n - data_template:\n payload: '{\"Protocol\":\"{{ protocol }}\",\"Bits\": {{ bits }},\"Data\": 0x{{"
},
{
"path": "hacs.json",
"chars": 88,
"preview": "{\n \"name\": \"Tasmota-IRHVAC\",\n \"render_readme\": true,\n \"homeassistant\": \"2024.11.0\"\n}\n"
}
]
About this extraction
This page contains the full source code of the hristo-atanasov/Tasmota-IRHVAC GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (81.4 KB), approximately 21.3k tokens, and a symbol index with 37 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.