master b6b8dba5fcbf cached
14 files
81.4 KB
21.3k tokens
37 symbols
1 requests
Download .txt
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"
}
Download .txt
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
Download .txt
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": "[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](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.

Copied to clipboard!