[
  {
    "path": ".github/workflows/validate.yml",
    "content": "name: Validate\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n\njobs:\n  validate-hacs:\n    runs-on: \"ubuntu-latest\"\n    steps:\n      - uses: \"actions/checkout@v3\"\n      - name: HACS validation\n        uses: \"hacs/action@main\"\n        with:\n          category: \"integration\""
  },
  {
    "path": "INFO.md",
    "content": "Tasmota-IRHVAC\nHome Assistant platform for controlling IR Air Conditioners via Tasmota IRHVAC command and compatible harware\n"
  },
  {
    "path": "README.md",
    "content": "[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)\n# Tasmota-IRHVAC\nHome Assistant platform for controlling IR Air Conditioners via Tasmota IRHVAC command and compatible hardware\n\nThis 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)\nThe 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*.\n\n![image1](/images/schematics.jpeg)\n\nTasmota configuration looks like this:\n\n![image2](/images/tasmota_config.jpeg)\n\nAfter configuration open Tasmota console, point your AC remote to the IR receiver and press the button for turning the AC on.\n\nIf everything in the above steps is made right, you should see a line like this (example with Fujitsu Air Conditioner):\n\n```javacript\n{'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}}}\n```\n\nIf vendor is not *‘Unknown’* and you see the *‘IRHVAC’* key, containing information, you can be sure that it will work for you.\n\nNext step is to download the files from this repo, get the folder named *\"tasmota_irhvac\"* and place it in your *\"custom_components\"* folder.\nReastart Home Assistant!\nAfter 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.\nUsing 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).\nCycle trough all of your AC modes and write them in supported_modes. I have left some possible values commented.\n\nCycle trough your fan speeds and and write them down in supported_fan_speeds\n\nIf your AC doesnt support horizontal swinging remove *-\"horizontal\"* and *-\"both\"* from *supported_swing_list*\n\nEnter your *hvac_model*\n\nChange the *“min_temp”* and *“max_temp”* values with your AC min and max temp.\n*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.\n*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.\nYou can also remove all lines that doesn’t need to be changed and are marked with “optional”.\nChange the *name* with the desired name.\nAfter 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.\n\nThis 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).\n\n![image2](/images/multisensors.jpeg)\n\nAs 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”*.\n\n```yaml\nir_code:\n  sequence:\n  - data_template:\n      payload: '{\"Protocol\":\"{{ protocol }}\",\"Bits\": {{ bits }},\"Data\": 0x{{ data }}}'\n      topic: 'cmnd/{{ room }}Multisensor/irsend'\n    service: mqtt.publish\nir_raw:\n  sequence:\n  - data_template:\n      payload: '0, {{ data }}'\n      topic: 'cmnd/{{ room }}Multisensor/irsend'\n    service: mqtt.publish\n```\n\nYou 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. :)\n\n```yaml\ncards:\n  - cards:\n      - action: service\n        color: white\n        icon: 'mdi:power'\n        name: Turn On Audio HEX\n        service:\n          action: ir_code\n          data:\n            bits: 12\n            data: A80\n            protocol: SONY\n            room: kitchen\n          domain: script\n        style:\n          - color: white\n          - background: green\n          - '--disabled-text-color': white\n        type: 'custom:button-card'\n      - action: service\n        color: white\n        icon: 'mdi:power'\n        name: Turn Off Audio HEX\n        service:\n          action: ir_code\n          data:\n            bits: 12\n            data: E85\n            protocol: SONY\n            room: kitchen\n          domain: script\n        style:\n          - color: white\n          - background: red\n          - '--disabled-text-color': white\n        type: 'custom:button-card'\n      - action: service\n        color: white\n        icon: 'mdi:power'\n        name: Test AC Raw\n        service:\n          action: ir_raw\n          data:\n            data: >-\n              3290, 1602,  424, 390,  424, 390,  424, 1232,  398, 390,  424,\n              1212,  420, 390,  424, 390,  424, 390,  424, 1232,  398, 1234, \n              398, 390,  424, 390,  426, 390,  424, 1232,  400, 1230,  398,\n              392,  424, 390,  426, 390,  426, 390,  424, 390,  424, 390,  424,\n              390,  424, 392,  424, 390,  424, 392,  424, 390,  424, 390,  424,\n              390,  424, 1232,  398, 390,  424, 390,  426, 390,  424, 390,  424,\n              392,  424, 390,  424, 392,  426, 1230,  400, 390,  424, 390,  426,\n              390,  424, 390,  424, 1232,  400, 1232,  398, 1232,  398, 1232, \n              400, 1232,  398, 1232,  400, 1232,  400, 1232,  400, 390,  426,\n              390,  424, 1206,  424, 390,  424, 390,  424, 392,  424, 390,  424,\n              392,  424, 390,  426, 390,  424, 390,  424, 1230,  402, 1230, \n              402, 390,  424, 390,  424, 1230,  402, 390,  424, 390,  424, 390, \n              424, 390,  424, 390,  426, 390,  424, 1230,  402, 1228,  402,\n              390,  424, 390,  424, 390,  426, 390,  424, 390,  426, 390,  424,\n              390,  424, 390,  426, 390,  426, 390,  424, 390,  424, 390,  426,\n              390,  424, 390,  424, 392,  426, 390,  424, 390,  424, 392,  424,\n              390,  424, 390,  424, 390,  424, 390,  424, 390,  424, 390,  424,\n              390,  424, 390,  426, 390,  426, 390,  424, 390,  424, 392,  424,\n              390,  424, 390,  424, 390,  424, 390,  424, 392,  424, 390,  424,\n              390,  424, 390,  426, 390,  424, 392,  424, 390,  424, 392,  424,\n              390,  424, 390,  424, 1228,  404, 388,  424, 390,  424, 392,  424,\n              1228,  404, 1228,  402, 1228,  402, 390,  426, 1228,  402, 390, \n              424, 390,  424\n            room: bedroom\n          domain: script\n        style:\n          - color: white\n          - background: blue\n          - '--disabled-text-color': white\n        type: 'custom:button-card'\n    type: vertical-stack\ntype: vertical-stack\n```\n\nMore 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)\n"
  },
  {
    "path": "SERVICES.md",
    "content": "# Services in Tasmota IRHVAC for HA v0.108+ and newer\nSupprort for setting econo, turbo, quiet, light, filters, clean, beep and sleep via newly added services\n\nIn 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.\nNewly added services are:\n\n***tasmota_irhvac.set_econo***\nwith payload of:\n```javacript\n{econo: \"on\", entity_id: clima.your_clima_entity_id}\n```\nwhere *econo* can be \"on\" or \"off\" and entity_id can be your climate entity_id, like, for example, *climate.kitchen_ac*\n\n***tasmota_irhvac.set_turbo***\nwith payload of:\n```javacript\n{turbo: \"on\", entity_id: clima.your_clima_entity_id}\n```\nwhere *turbo* can be \"on\" or \"off\" and entity_id can be your climate entity_id, like, for example, *climate.kitchen_ac*\n***tasmota_irhvac.set_quiet***\nwith payload of:\n```javacript\n{quiet: \"on\", entity_id: clima.your_clima_entity_id}\n```\nwhere *quiet* can be \"on\" or \"off\" and entity_id can be your climate entity_id, like, for example, *climate.kitchen_ac*\n\n***tasmota_irhvac.set_light***\nwith payload of:\n```javacript\n{light: \"on\", entity_id: clima.your_clima_entity_id}\n```\nwhere *light:* can be \"on\" or \"off\" and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac*\n\n***tasmota_irhvac.set_filters***\nwith payload of:\n```javacript\n{filters: \"on\", entity_id: clima.your_clima_entity_id}\n```\nwhere *filters:* can be \"on\" or \"off\" and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac*\n* Note that it is **filters** instead of **filter**, because \"filter\" is reserved word and we cannot use it.*\n\n***tasmota_irhvac.set_clean***\nwith payload of:\n```javacript\n{clean: \"on\", entity_id: clima.your_clima_entity_id}\n```\nwhere *clean:* can be \"on\" or \"off\" and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac*\n\n***tasmota_irhvac.set_beep***\nwith payload of:\n```javacript\n{beep: \"on\", entity_id: clima.your_clima_entity_id}\n```\nwhere *beep:* can be \"on\" or \"off\" and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac*\n\n***tasmota_irhvac.set_sleep***\nwith payload of:\n```javacript\n{sleep: \"-1\", entity_id: clima.your_clima_entity_id}\n```\nwhere *sleep:* can be any string, that your AC supports, and *entity_id:* can be your climate entity_id, like, for example, *climate.kitchen_ac*\n\n# Example with Template Switch\nExample from **configuration.yaml**. Please, use only these services, that are supported from your AC!\n\n```yaml\nswitch:\n  - platform: template\n    switches:\n      kitchen_climate_econo:\n        friendly_name: \"Econo\"\n        value_template: \"{{ is_state_attr('climate.kitchen_ac', 'econo', 'on') }}\"\n        turn_on:\n          service: tasmota_irhvac.set_econo\n          data:\n            entity_id: climate.kitchen_ac\n            econo: 'on'\n        turn_off:\n          service: tasmota_irhvac.set_econo\n          data:\n            entity_id: climate.kitchen_ac\n            econo: 'off'\n  - platform: template\n    switches:\n      kitchen_climate_turbo:\n        friendly_name: \"Turbo\"\n        value_template: \"{{ is_state_attr('climate.kitchen_ac', 'turbo', 'on') }}\"\n        turn_on:\n          service: tasmota_irhvac.set_turbo\n          data:\n            entity_id: climate.kitchen_ac\n            turbo: 'on'\n        turn_off:\n          service: tasmota_irhvac.set_turbo\n          data:\n            entity_id: climate.kitchen_ac\n            turbo: 'off'\n  - platform: template\n    switches:\n      kitchen_climate_quiet:\n        friendly_name: \"Quiet\"\n        value_template: \"{{ is_state_attr('climate.kitchen_ac', 'quiet', 'on') }}\"\n        turn_on:\n          service: tasmota_irhvac.set_quiet\n          data:\n            entity_id: climate.kitchen_ac\n            quiet: 'on'\n        turn_off:\n          service: tasmota_irhvac.set_quiet\n          data:\n            entity_id: climate.kitchen_ac\n            quiet: 'off'\n  - platform: template\n    switches:\n      kitchen_climate_light:\n        friendly_name: \"Light\"\n        value_template: \"{{ is_state_attr('climate.kitchen_ac', 'light', 'on') }}\"\n        turn_on:\n          service: tasmota_irhvac.set_light\n          data:\n            entity_id: climate.kitchen_ac\n            light: 'on'\n        turn_off:\n          service: tasmota_irhvac.set_light\n          data:\n            entity_id: climate.kitchen_ac\n            light: 'off'\n  - platform: template\n    switches:\n      kitchen_climate_filter:\n        friendly_name: \"Filter\"\n        value_template: \"{{ is_state_attr('climate.kitchen_ac', 'filters', 'on') }}\"\n        turn_on:\n          service: tasmota_irhvac.set_filters\n          data:\n            entity_id: climate.kitchen_ac\n            filters: 'on'\n        turn_off:\n          service: tasmota_irhvac.set_filters\n          data:\n            entity_id: climate.kitchen_ac\n            filters: 'off'\n  - platform: template\n    switches:\n      kitchen_climate_clean:\n        friendly_name: \"Clean\"\n        value_template: \"{{ is_state_attr('climate.kitchen_ac', 'clean', 'on') }}\"\n        turn_on:\n          service: tasmota_irhvac.set_clean\n          data:\n            entity_id: climate.kitchen_ac\n            clean: 'on'\n        turn_off:\n          service: tasmota_irhvac.set_clean\n          data:\n            entity_id: climate.kitchen_ac\n            clean: 'off'\n  - platform: template\n    switches:\n      kitchen_climate_beep:\n        friendly_name: \"Beep\"\n        value_template: \"{{ is_state_attr('climate.kitchen_ac', 'beep', 'on') }}\"\n        turn_on:\n          service: tasmota_irhvac.set_beep\n          data:\n            entity_id: climate.kitchen_ac\n            beep: 'on'\n        turn_off:\n          service: tasmota_irhvac.set_beep\n          data:\n            entity_id: climate.kitchen_ac\n            beep: 'off'\n  - platform: template\n    switches:\n      kitchen_climate_sleep:\n        friendly_name: \"Sleep\"\n        value_template: \"{{ is_state_attr('climate.kitchen_ac', 'sleep', '0') }}\"\n        turn_on:\n          service: tasmota_irhvac.set_sleep\n          data:\n            entity_id: climate.kitchen_ac\n            sleep: '1'\n        turn_off:\n          service: tasmota_irhvac.set_sleep\n          data:\n            entity_id: climate.kitchen_ac\n            sleep: '0'\n```\n"
  },
  {
    "path": "blueprints/automation/tasmota_irhvac/climate_vane_control_tasmota-irhvac.yaml",
    "content": "blueprint:\n  name: Climate Vane Control - Tasmota-IRHVAC\n  description: Contoroling vertical or horizontal vane swing, potition of Climate on Tasmota-IRHVAC\n  domain: automation\n  source_url: https://github.com/hristo-atanasov/Tasmota-IRHVAC/blob/master/blueprints/automation/tasmota_irhvac/climate_vane_control_tasmota-irhvac.yaml\n  input:\n    climate_entity:\n      name: Target Climate\n      selector:\n        entity:\n          integration: tasmota_irhvac\n          domain: climate\n    input_entity:\n      name: Dropdown Input Helper\n      selector:\n        entity:\n          domain: input_select\n      description: \"The dropdown input helper entity can have the following options if the device supports it.\n\n### Vertical Vane\n\n- off\n\n- auto\n\n- highest\n\n- high\n\n- low\n\n- lowest\n\n\n### Horizontal Vane\n\n- off\n\n- auto\n\n- left max\n\n- left\n\n- middle\n\n- right\n\n- right max\n\n- wide\n\n\"\n    vane:\n      name: Target Vane\n      default: Vertical\n      selector:\n        select:\n          options:\n            - Vertical\n            - Horizontal\n\nmode: parallel\nmax: 2\n\nvariables:\n  vane:\n    attr:\n      Vertical: swingv\n      Horizontal: swingh\n    srv:\n      Vertical: tasmota_irhvac.set_swingv\n      Horizontal: tasmota_irhvac.set_swingh\n  key: !input vane\n  climate_entity: !input climate_entity\n\ntrigger:\n  - platform: state\n    entity_id:\n      - !input climate_entity\n    attribute: swingv\n    id: Vertical\n  - platform: state\n    entity_id:\n      - !input climate_entity\n    attribute: swingh\n    id: Horizontal\n  - platform: state\n    entity_id:\n      - !input input_entity\n    id: set\n\naction:\n  - if:\n      - condition: trigger\n        id: set\n      - condition: template\n        value_template: \"{{state_attr(climate_entity, vane.attr[key]) != states(trigger.entity_id)}}\"\n    then:\n      - if:\n          - condition: template\n            value_template: \"{{key == 'Vertical'}}\"\n        then:\n          - service: \"{{vane.srv[key]}}\"\n            data:\n              entity_id: !input climate_entity\n              swingv: \"{{states(trigger.entity_id)}}\"\n        else:\n          - service: \"{{vane.srv[key]}}\"\n            data:\n              entity_id: !input climate_entity\n              swingh: \"{{states(trigger.entity_id)}}\"\n  - if:\n      - condition: template\n        value_template: '{{key == trigger.id}}'\n    then:\n      - service: input_select.select_option\n        data:\n          option: \"{{state_attr(climate_entity, vane.attr[key])}}\"\n        target:\n          entity_id: !input input_entity\n"
  },
  {
    "path": "custom_components/tasmota_irhvac/__init__.py",
    "content": "\"\"\"The Tasmota Irhvac climate component.\"\"\"\n"
  },
  {
    "path": "custom_components/tasmota_irhvac/climate.py",
    "content": "\"\"\"Adds support for generic thermostat units.\"\"\"\n\nimport asyncio\nimport json\nimport logging\nimport uuid\n\nimport homeassistant.helpers.config_validation as cv\nimport homeassistant.util.dt as dt_util\nimport voluptuous as vol\nfrom homeassistant.components import mqtt\n\ntry:\n    from homeassistant.components.mqtt.schemas import MQTT_ENTITY_COMMON_SCHEMA\nexcept ImportError:\n    from homeassistant.components.mqtt.mixins import MQTT_ENTITY_COMMON_SCHEMA\n\nfrom homeassistant.components.climate import PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA\n\n# try:\n#     from homeassistant.components.climate import ClimateEntity\n# except ImportError:\n#     from homeassistant.components.binary_sensor import ClimateDevice as ClimateEntity\nfrom homeassistant.components.climate import ClimateEntity\nfrom homeassistant.components.climate.const import (\n    ATTR_FAN_MODE,\n    ATTR_HVAC_MODE,\n    ATTR_PRESET_MODE,\n    ATTR_SWING_MODE,\n    FAN_AUTO,\n    FAN_DIFFUSE,\n    FAN_FOCUS,\n    FAN_TOP,\n    FAN_HIGH,\n    FAN_LOW,\n    FAN_MEDIUM,\n    FAN_MIDDLE,\n    FAN_OFF,\n    FAN_ON,\n    PRESET_AWAY,\n    PRESET_NONE,\n    SWING_BOTH,\n    SWING_HORIZONTAL,\n    SWING_OFF,\n    SWING_VERTICAL,\n    ClimateEntityFeature,\n    HVACAction,\n    HVACMode,\n)\nfrom homeassistant.const import (\n    ATTR_ENTITY_ID,\n    ATTR_TEMPERATURE,\n    CONF_NAME,\n    CONF_UNIQUE_ID,\n    PRECISION_HALVES,\n    PRECISION_TENTHS,\n    PRECISION_WHOLE,\n    STATE_OFF,\n    STATE_ON,\n    STATE_UNAVAILABLE,\n    STATE_UNKNOWN,\n    UnitOfTemperature,\n)\nfrom homeassistant.core import cached_property, callback\nfrom homeassistant.helpers import event as ha_event\nfrom homeassistant.helpers.restore_state import RestoreEntity\nfrom homeassistant.util.unit_conversion import TemperatureConverter\n\nfrom .const import (\n    ATTR_BEEP,\n    ATTR_CLEAN,\n    ATTR_ECONO,\n    ATTR_FILTERS,\n    ATTR_LAST_ON_MODE,\n    ATTR_LIGHT,\n    ATTR_QUIET,\n    ATTR_SLEEP,\n    ATTR_STATE_MODE,\n    ATTR_SWINGH,\n    ATTR_SWINGV,\n    ATTR_TURBO,\n    ATTRIBUTES_IRHVAC,\n    CONF_AVAILABILITY_TOPIC,\n    CONF_AWAY_TEMP,\n    CONF_BEEP,\n    CONF_CELSIUS,\n    CONF_CLEAN,\n    CONF_COMMAND_TOPIC,\n    CONF_ECONO,\n    CONF_EXCLUSIVE_GROUP_VENDOR,\n    CONF_FAN_LIST,\n    CONF_FILTER,\n    CONF_HUMIDITY_SENSOR,\n    CONF_IGNORE_OFF_TEMP,\n    CONF_INITIAL_OPERATION_MODE,\n    CONF_KEEP_MODE,\n    CONF_LIGHT,\n    CONF_MAX_TEMP,\n    CONF_MIN_TEMP,\n    CONF_MODEL,\n    CONF_MODES_LIST,\n    CONF_MQTT_DELAY,\n    CONF_POWER_SENSOR,\n    CONF_PRECISION,\n    CONF_PROTOCOL,\n    CONF_QUIET,\n    CONF_SLEEP,\n    CONF_SPECIAL_MODE,\n    CONF_STATE_TOPIC,\n    CONF_SWING_LIST,\n    CONF_SWINGH,\n    CONF_SWINGV,\n    CONF_TARGET_TEMP,\n    CONF_TEMP_SENSOR,\n    CONF_TEMP_STEP,\n    CONF_TOGGLE_LIST,\n    CONF_TURBO,\n    CONF_VENDOR,\n    DATA_KEY,\n    DEFAULT_COMMAND_TOPIC,\n    DEFAULT_CONF_BEEP,\n    DEFAULT_CONF_CELSIUS,\n    DEFAULT_CONF_CLEAN,\n    DEFAULT_CONF_ECONO,\n    DEFAULT_CONF_FILTER,\n    DEFAULT_CONF_KEEP_MODE,\n    DEFAULT_CONF_LIGHT,\n    DEFAULT_CONF_MODEL,\n    DEFAULT_CONF_QUIET,\n    DEFAULT_CONF_SLEEP,\n    DEFAULT_CONF_TURBO,\n    DEFAULT_FAN_LIST,\n    DEFAULT_IGNORE_OFF_TEMP,\n    DEFAULT_MAX_TEMP,\n    DEFAULT_MIN_TEMP,\n    DEFAULT_MQTT_DELAY,\n    DEFAULT_NAME,\n    DEFAULT_PRECISION,\n    DEFAULT_STATE_MODE,\n    DEFAULT_STATE_TOPIC,\n    DEFAULT_TARGET_TEMP,\n    DOMAIN,\n    HVAC_FAN_AUTO,\n    HVAC_FAN_AUTO_MAX,\n    HVAC_FAN_MAX,\n    HVAC_FAN_MAX_HIGH,\n    HVAC_FAN_MEDIUM,\n    HVAC_FAN_MIN,\n    HVAC_MODE_AUTO_FAN,\n    HVAC_MODE_FAN_AUTO,\n    HVAC_MODES,\n    ON_OFF_LIST,\n    SERVICE_BEEP_MODE,\n    SERVICE_CLEAN_MODE,\n    SERVICE_ECONO_MODE,\n    SERVICE_FILTERS_MODE,\n    SERVICE_LIGHT_MODE,\n    SERVICE_QUIET_MODE,\n    SERVICE_SET_SWINGH,\n    SERVICE_SET_SWINGV,\n    SERVICE_SLEEP_MODE,\n    SERVICE_TURBO_MODE,\n    STATE_AUTO,\n    STATE_MODE_LIST,\n    TOGGLE_ALL_LIST,\n)\n\nDEFAULT_MODES_LIST = [\n    HVACMode.COOL,\n    HVACMode.HEAT,\n    HVACMode.DRY,\n    HVAC_MODE_AUTO_FAN,\n    HVAC_MODE_FAN_AUTO,\n]\n\nDEFAULT_SWING_LIST = [SWING_OFF, SWING_VERTICAL]\nDEFAULT_INITIAL_OPERATION_MODE = HVACMode.OFF\n\n_LOGGER = logging.getLogger(__name__)\n\nSUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE\n\nif hasattr(ClimateEntityFeature, \"TURN_ON\"):\n    SUPPORT_FLAGS |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF\n\nPLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend(\n    {\n        vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string,\n        vol.Optional(CONF_UNIQUE_ID): cv.string,\n        vol.Exclusive(CONF_VENDOR, CONF_EXCLUSIVE_GROUP_VENDOR): cv.string,\n        vol.Exclusive(CONF_PROTOCOL, CONF_EXCLUSIVE_GROUP_VENDOR): cv.string,\n        vol.Required(\n            CONF_COMMAND_TOPIC, default=DEFAULT_COMMAND_TOPIC\n        ): mqtt.valid_publish_topic,\n        vol.Optional(CONF_AVAILABILITY_TOPIC): mqtt.util.valid_topic,\n        vol.Optional(CONF_TEMP_SENSOR): cv.entity_id,\n        vol.Optional(CONF_HUMIDITY_SENSOR): cv.entity_id,\n        vol.Optional(CONF_POWER_SENSOR): cv.entity_id,\n        vol.Optional(\n            CONF_STATE_TOPIC, default=DEFAULT_STATE_TOPIC\n        ): mqtt.valid_subscribe_topic,\n        vol.Optional(CONF_STATE_TOPIC + \"_2\"): mqtt.util.valid_topic,\n        vol.Optional(CONF_MQTT_DELAY, default=DEFAULT_MQTT_DELAY): vol.Coerce(float),\n        vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float),\n        vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),\n        vol.Optional(CONF_TARGET_TEMP, default=DEFAULT_TARGET_TEMP): vol.Coerce(float),\n        vol.Optional(\n            CONF_INITIAL_OPERATION_MODE, default=DEFAULT_INITIAL_OPERATION_MODE\n        ): vol.In(HVAC_MODES),\n        vol.Optional(CONF_AWAY_TEMP): vol.Coerce(float),\n        vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.In(\n            [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]\n        ),\n        vol.Optional(CONF_TEMP_STEP, default=PRECISION_WHOLE): vol.In(\n            [PRECISION_HALVES, PRECISION_WHOLE]\n        ),\n        vol.Optional(CONF_MODES_LIST, default=DEFAULT_MODES_LIST): vol.All(\n            cv.ensure_list, [vol.In(HVAC_MODES)]\n        ),\n        vol.Optional(CONF_FAN_LIST, default=DEFAULT_FAN_LIST): vol.All(\n            cv.ensure_list,\n            [\n                vol.In(\n                    [\n                        FAN_ON,\n                        FAN_OFF,\n                        FAN_AUTO,\n                        FAN_LOW,\n                        FAN_MEDIUM,\n                        FAN_HIGH,\n                        FAN_MIDDLE,\n                        FAN_FOCUS,\n                        FAN_DIFFUSE,\n                        FAN_TOP,\n                        HVAC_FAN_MIN,\n                        HVAC_FAN_MEDIUM,\n                        HVAC_FAN_MAX,\n                        HVAC_FAN_AUTO,\n                        HVAC_FAN_MAX_HIGH,\n                        HVAC_FAN_AUTO_MAX,\n                    ]\n                )\n            ],\n        ),\n        vol.Optional(CONF_SWING_LIST, default=DEFAULT_SWING_LIST): vol.All(\n            cv.ensure_list,\n            [vol.In([SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL])],\n        ),\n        vol.Optional(CONF_QUIET, default=DEFAULT_CONF_QUIET): cv.string,\n        vol.Optional(CONF_TURBO, default=DEFAULT_CONF_TURBO): cv.string,\n        vol.Optional(CONF_ECONO, default=DEFAULT_CONF_ECONO): cv.string,\n        vol.Optional(CONF_MODEL, default=DEFAULT_CONF_MODEL): cv.string,\n        vol.Optional(CONF_CELSIUS, default=DEFAULT_CONF_CELSIUS): cv.string,\n        vol.Optional(CONF_LIGHT, default=DEFAULT_CONF_LIGHT): cv.string,\n        vol.Optional(CONF_FILTER, default=DEFAULT_CONF_FILTER): cv.string,\n        vol.Optional(CONF_CLEAN, default=DEFAULT_CONF_CLEAN): cv.string,\n        vol.Optional(CONF_BEEP, default=DEFAULT_CONF_BEEP): cv.string,\n        vol.Optional(CONF_SLEEP, default=DEFAULT_CONF_SLEEP): cv.string,\n        vol.Optional(CONF_KEEP_MODE, default=DEFAULT_CONF_KEEP_MODE): cv.boolean,\n        vol.Optional(CONF_SWINGV): cv.string,\n        vol.Optional(CONF_SWINGH): cv.string,\n        vol.Optional(CONF_TOGGLE_LIST, default=[]): vol.All(\n            cv.ensure_list,\n            [vol.In(TOGGLE_ALL_LIST)],\n        ),\n        vol.Optional(CONF_IGNORE_OFF_TEMP, default=DEFAULT_IGNORE_OFF_TEMP): cv.boolean,\n        vol.Optional(CONF_SPECIAL_MODE, default=\"\"): cv.string,\n    }\n)\n\nPLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(MQTT_ENTITY_COMMON_SCHEMA.schema)\nif hasattr(mqtt, \"MQTT_BASE_PLATFORM_SCHEMA\"):\n    PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.MQTT_BASE_PLATFORM_SCHEMA.schema)\nelse:\n    PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.config.MQTT_BASE_SCHEMA.schema)\n\nIRHVAC_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids})\n\nSERVICE_SCHEMA_ECONO_MODE = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_ECONO): vol.In(ON_OFF_LIST),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_TURBO_MODE = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_TURBO): vol.In(ON_OFF_LIST),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_QUIET_MODE = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_QUIET): vol.In(ON_OFF_LIST),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_LIGHT_MODE = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_LIGHT): vol.In(ON_OFF_LIST),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_FILTERS_MODE = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_FILTERS): vol.In(ON_OFF_LIST),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_CLEAN_MODE = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_CLEAN): vol.In(ON_OFF_LIST),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_BEEP_MODE = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_BEEP): vol.In(ON_OFF_LIST),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_SLEEP_MODE = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_SLEEP): cv.string,\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_SET_SWINGV = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_SWINGV): vol.In(\n            [\"off\", \"auto\", \"highest\", \"high\", \"middle\", \"low\", \"lowest\"]\n        ),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\nSERVICE_SCHEMA_SET_SWINGH = IRHVAC_SERVICE_SCHEMA.extend(\n    {\n        vol.Required(ATTR_SWINGH): vol.In(\n            [\"off\", \"auto\", \"left max\", \"left\", \"middle\", \"right\", \"right max\", \"wide\"]\n        ),\n        vol.Optional(ATTR_STATE_MODE, default=DEFAULT_STATE_MODE): vol.In(\n            STATE_MODE_LIST\n        ),\n    }\n)\n\nSERVICE_TO_METHOD = {\n    SERVICE_ECONO_MODE: {\n        \"method\": \"async_set_econo\",\n        \"schema\": SERVICE_SCHEMA_ECONO_MODE,\n    },\n    SERVICE_TURBO_MODE: {\n        \"method\": \"async_set_turbo\",\n        \"schema\": SERVICE_SCHEMA_TURBO_MODE,\n    },\n    SERVICE_QUIET_MODE: {\n        \"method\": \"async_set_quiet\",\n        \"schema\": SERVICE_SCHEMA_QUIET_MODE,\n    },\n    SERVICE_LIGHT_MODE: {\n        \"method\": \"async_set_light\",\n        \"schema\": SERVICE_SCHEMA_LIGHT_MODE,\n    },\n    SERVICE_FILTERS_MODE: {\n        \"method\": \"async_set_filters\",\n        \"schema\": SERVICE_SCHEMA_FILTERS_MODE,\n    },\n    SERVICE_CLEAN_MODE: {\n        \"method\": \"async_set_clean\",\n        \"schema\": SERVICE_SCHEMA_CLEAN_MODE,\n    },\n    SERVICE_BEEP_MODE: {\n        \"method\": \"async_set_beep\",\n        \"schema\": SERVICE_SCHEMA_BEEP_MODE,\n    },\n    SERVICE_SLEEP_MODE: {\n        \"method\": \"async_set_sleep\",\n        \"schema\": SERVICE_SCHEMA_SLEEP_MODE,\n    },\n    SERVICE_SET_SWINGV: {\n        \"method\": \"async_set_swingv\",\n        \"schema\": SERVICE_SCHEMA_SET_SWINGV,\n    },\n    SERVICE_SET_SWINGH: {\n        \"method\": \"async_set_swingh\",\n        \"schema\": SERVICE_SCHEMA_SET_SWINGH,\n    },\n}\n\n\nasync def async_setup_platform(hass, config, async_add_entities, discovery_info=None):\n    \"\"\"Set up the generic thermostat platform.\"\"\"\n    vendor = config.get(CONF_VENDOR)\n    protocol = config.get(CONF_PROTOCOL)\n    name = config.get(CONF_NAME)\n\n    if DATA_KEY not in hass.data:\n        hass.data[DATA_KEY] = {}\n\n    if vendor is None:\n        if protocol is None:\n            _LOGGER.error('Neither vendor nor protocol provided for \"%s\"!', name)\n            return\n\n        vendor = protocol\n\n    tasmotaIrhvac = TasmotaIrhvac(\n        hass,\n        vendor,\n        config,\n    )\n    uuidstr = uuid.uuid4().hex\n    hass.data[DATA_KEY][uuidstr] = tasmotaIrhvac\n\n    async_add_entities([tasmotaIrhvac])\n\n    async def async_service_handler(service):\n        \"\"\"Map services to methods on TasmotaIrhvac.\"\"\"\n        method = SERVICE_TO_METHOD.get(service.service, {})\n        params = {\n            key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID\n        }\n        entity_ids = service.data.get(ATTR_ENTITY_ID)\n        if entity_ids:\n            devices = [\n                device\n                for device in hass.data[DATA_KEY].values()\n                if device.entity_id in entity_ids\n            ]\n        else:\n            devices = hass.data[DATA_KEY].values()\n\n        update_tasks = []\n        for device in devices:\n            if not hasattr(device, method[\"method\"]):\n                continue\n            await getattr(device, method[\"method\"])(**params)\n            update_tasks.append(asyncio.create_task(device.async_update_ha_state(True)))\n\n        if update_tasks:\n            await asyncio.wait(update_tasks)\n\n    for irhvac_service in SERVICE_TO_METHOD:\n        schema = SERVICE_TO_METHOD[irhvac_service].get(\"schema\", IRHVAC_SERVICE_SCHEMA)\n        hass.services.async_register(\n            DOMAIN, irhvac_service, async_service_handler, schema=schema\n        )\n\n\nclass TasmotaIrhvac(RestoreEntity, ClimateEntity):\n    \"\"\"Representation of a Generic Thermostat device.\"\"\"\n\n    # It can remove from HA >= 2025.1\n    # see https://developers.home-assistant.io/blog/2024/01/24/climate-climateentityfeatures-expanded/\n    _enable_turn_on_off_backwards_compatibility = False\n\n    _last_on_mode: HVACMode | None\n\n    def __init__(\n        self,\n        hass,\n        vendor,\n        config,\n    ):\n        \"\"\"Initialize the thermostat.\"\"\"\n        self.topic = config.get(CONF_COMMAND_TOPIC)\n        self.hass = hass\n        self._vendor = vendor\n        self._temp_sensor = config.get(CONF_TEMP_SENSOR)\n        self._humidity_sensor = config.get(CONF_HUMIDITY_SENSOR)\n        self._power_sensor = config.get(CONF_POWER_SENSOR)\n        self.state_topic = config[CONF_STATE_TOPIC]\n        self.state_topic2 = config.get(CONF_STATE_TOPIC + \"_2\")\n        self._away_temp = config.get(CONF_AWAY_TEMP)\n        self._saved_target_temp = config[CONF_TARGET_TEMP] or self._away_temp\n        self._temp_precision = config[CONF_PRECISION]\n        self._enabled = False\n        self.power_mode = None\n        self._active = False\n        self._mqtt_delay = config[CONF_MQTT_DELAY]\n        self._min_temp = config[CONF_MIN_TEMP]\n        self._max_temp = config[CONF_MAX_TEMP]\n        self._def_target_temp = config[CONF_TARGET_TEMP]\n        self._is_away = False\n        self._modes_list = config[CONF_MODES_LIST]\n        self._quiet = config[CONF_QUIET].lower()\n        self._turbo = config[CONF_TURBO].lower()\n        self._econo = config[CONF_ECONO].lower()\n        self._model = config[CONF_MODEL]\n        self._celsius = config[CONF_CELSIUS]\n        self._light = config[CONF_LIGHT].lower()\n        self._filter = config[CONF_FILTER].lower()\n        self._clean = config[CONF_CLEAN].lower()\n        self._beep = config[CONF_BEEP].lower()\n        self._sleep = config[CONF_SLEEP].lower()\n        self._sub_state = None\n        self._keep_mode = config[CONF_KEEP_MODE]\n        self._last_on_mode = None\n        self._swingv = (\n            config.get(CONF_SWINGV).lower()\n            if config.get(CONF_SWINGV) is not None\n            else None\n        )\n        self._swingh = (\n            config.get(CONF_SWINGH).lower()\n            if config.get(CONF_SWINGH) is not None\n            else None\n        )\n        self._fix_swingv = None\n        self._fix_swingh = None\n        self._toggle_list = config[CONF_TOGGLE_LIST]\n        self._state_mode = DEFAULT_STATE_MODE\n        self._ignore_off_temp = config[CONF_IGNORE_OFF_TEMP]\n        self._special_mode = config[CONF_SPECIAL_MODE]\n        self._use_track_state_change_event = False\n        self._unsubscribes = []\n\n        self.availability_topic = config.get(CONF_AVAILABILITY_TOPIC)\n        if (self.availability_topic) is None:\n            path = self.topic.split(\"/\")\n            self.availability_topic = \"tele/\" + path[1] + \"/LWT\"\n\n        # Set _attr_*\n        self._attr_unique_id = config.get(CONF_UNIQUE_ID)\n        self._attr_name = config.get(CONF_NAME)\n        self._attr_should_poll = False\n        self._attr_temperature_unit = (\n            UnitOfTemperature.CELSIUS\n            if self._celsius.lower() == \"on\"\n            else UnitOfTemperature.FAHRENHEIT\n        )\n        self._attr_hvac_mode = config.get(CONF_INITIAL_OPERATION_MODE)\n        self._attr_target_temperature_step = config[CONF_TEMP_STEP]\n        self._attr_hvac_modes = config[CONF_MODES_LIST]\n        self.use_electra_tweak = False\n        self._attr_fan_modes = config.get(CONF_FAN_LIST)\n        if (\n            isinstance(self._attr_fan_modes, list)\n            and HVAC_FAN_MAX_HIGH in self._attr_fan_modes\n            and HVAC_FAN_AUTO_MAX in self._attr_fan_modes\n        ):\n            self.use_electra_tweak = True\n            new_fan_list = []\n            for val in self._attr_fan_modes:\n                if val == HVAC_FAN_MAX_HIGH:\n                    new_fan_list.append(FAN_HIGH)\n                elif val == HVAC_FAN_AUTO_MAX:\n                    new_fan_list.append(HVAC_FAN_MAX)\n                else:\n                    new_fan_list.append(val)\n            self._attr_fan_modes = new_fan_list if len(new_fan_list) else None\n        self._attr_fan_mode = (\n            self._attr_fan_modes[0]\n            if isinstance(self._attr_fan_modes, list) and len(self._attr_fan_modes)\n            else None\n        )\n        self._attr_swing_modes = config.get(CONF_SWING_LIST)\n        self._attr_swing_mode = (\n            self._attr_swing_modes[0]\n            if isinstance(self._attr_swing_modes, list) and len(self._attr_swing_modes)\n            else None\n        )\n        self._attr_preset_modes = (\n            [PRESET_NONE, PRESET_AWAY] if self._away_temp else None\n        )\n        self._attr_preset_mode = None\n        self._attr_current_temperature = None\n        self._attr_current_humidity = None\n        self._attr_target_temperature = self._def_target_temp\n\n        self._support_flags = SUPPORT_FLAGS\n        if self._away_temp is not None:\n            self._support_flags = self._support_flags | ClimateEntityFeature.PRESET_MODE\n        if self._attr_swing_mode is not None:\n            self._support_flags = self._support_flags | ClimateEntityFeature.SWING_MODE\n\n    async def async_added_to_hass(self):\n        # Replacing `async_track_state_change` with `async_track_state_change_event`\n        # See, https://developers.home-assistant.io/blog/2024/04/13/deprecate_async_track_state_change/\n        if hasattr(ha_event, \"async_track_state_change_event\"):\n            self._use_track_state_change_event = True\n\n        def regist_track_state_change_event(entity_id):\n            if self._use_track_state_change_event:\n                ha_event.async_track_state_change_event(\n                    self.hass, entity_id, self._async_sensor_changed\n                )\n            else:\n                ha_event.async_track_state_change(\n                    self.hass, entity_id, self._async_sensor_changed\n                )\n\n        # Make sure MQTT integration is enabled and the client is available\n        await mqtt.async_wait_for_mqtt_client(self.hass)\n\n        \"\"\"Run when entity about to be added.\"\"\"\n        await super().async_added_to_hass()\n\n        # Add listener\n        self._unsubscribes = await self._subscribe_topics()\n\n        # Check If we have an old state\n        old_state = await self.async_get_last_state()\n        if old_state is not None:\n            # If we have no initial temperature, restore\n            if old_state.attributes.get(ATTR_TEMPERATURE) is not None:\n                self._attr_target_temperature = TemperatureConverter.convert(\n                    float(old_state.attributes[ATTR_TEMPERATURE]),\n                    self.hass.config.units.temperature_unit,\n                    self.temperature_unit,\n                )\n            if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY:\n                self._is_away = True\n            if old_state.attributes.get(ATTR_FAN_MODE) is not None:\n                self._attr_fan_mode = old_state.attributes.get(ATTR_FAN_MODE)\n            if old_state.attributes.get(ATTR_SWING_MODE) is not None:\n                self._attr_swing_mode = old_state.attributes.get(ATTR_SWING_MODE)\n            if old_state.attributes.get(ATTR_LAST_ON_MODE) is not None:\n                self._last_on_mode = old_state.attributes.get(ATTR_LAST_ON_MODE)\n\n            for attr, prop in ATTRIBUTES_IRHVAC.items():\n                val = old_state.attributes.get(attr)\n                if val is not None:\n                    setattr(self, \"_\" + prop, val)\n            if old_state.state:\n                self._attr_hvac_mode = (\n                    HVACMode.OFF\n                    if old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]\n                    else old_state.state\n                )\n                self._enabled = self._attr_hvac_mode != HVACMode.OFF\n                if self._enabled:\n                    self._last_on_mode = self._attr_hvac_mode\n            if self._swingv != \"auto\":\n                self._fix_swingv = self._swingv\n            if self._swingh != \"auto\":\n                self._fix_swingh = self._swingh\n\n        # No previous target temperature, try and restore defaults\n        if self._attr_target_temperature is None or self._attr_target_temperature < 1:\n            self._attr_target_temperature = self._def_target_temp\n            _LOGGER.warning(\n                \"No previously saved target temperature, setting to default value %s\",\n                self._attr_target_temperature,\n            )\n            self.async_write_ha_state()\n\n        if self._attr_hvac_mode == HVACMode.OFF:\n            self.power_mode = STATE_OFF\n            self._enabled = False\n        else:\n            self.power_mode = STATE_ON\n            self._enabled = True\n\n        for key in self._toggle_list:\n            setattr(self, \"_\" + key.lower(), \"off\")\n\n        if self._temp_sensor:\n            regist_track_state_change_event(self._temp_sensor)\n\n            temp_sensor_state = self.hass.states.get(self._temp_sensor)\n            if (\n                temp_sensor_state\n                and temp_sensor_state.state != STATE_UNKNOWN\n                and temp_sensor_state.state != STATE_UNAVAILABLE\n            ):\n                self._async_update_temp(temp_sensor_state)\n\n        if self._humidity_sensor:\n            regist_track_state_change_event(self._humidity_sensor)\n\n            humidity_sensor_state = self.hass.states.get(self._humidity_sensor)\n            if (\n                humidity_sensor_state\n                and humidity_sensor_state.state != STATE_UNKNOWN\n                and humidity_sensor_state.state != STATE_UNAVAILABLE\n            ):\n                self._async_update_humidity(humidity_sensor_state)\n\n        if self._power_sensor:\n            regist_track_state_change_event(self._power_sensor)\n\n    async def _subscribe_topics(self):\n        \"\"\"(Re)Subscribe to topics.\"\"\"\n\n        @callback\n        async def available_message_received(message: mqtt.ReceiveMessage) -> None:\n            msg = message.payload\n            _LOGGER.debug(msg)\n            if msg == \"Online\" or msg == \"Offline\":\n                self._attr_available = True if msg == \"Online\" else False\n                self.async_schedule_update_ha_state()\n\n        @callback\n        async def state_message_received(message: mqtt.ReceiveMessage) -> None:\n            \"\"\"Handle new MQTT state messages.\"\"\"\n            try:\n                json_payload = json.loads(message.payload)\n            except ValueError:\n                _LOGGER.error(\"Unable to parse MQTT payload as JSON: %s\", message.payload)\n                return\n            _LOGGER.debug(json_payload)\n\n            # If listening to `tele`, result looks like: {\"IrReceived\":{\"Protocol\":\"XXX\", ... ,\"IRHVAC\":{ ... }}}\n            # we want to extract the data.\n            if \"IrReceived\" in json_payload:\n                json_payload = json_payload[\"IrReceived\"]\n\n            # By now the payload must include an `IRHVAC` field.\n            if \"IRHVAC\" not in json_payload:\n                return\n\n            payload = json_payload[\"IRHVAC\"]\n\n            if payload[\"Vendor\"] == self._vendor:\n                # All values in the payload are Optional\n                prev_power = self.power_mode\n                if \"Power\" in payload:\n                    self.power_mode = payload[\"Power\"].lower()\n                if \"Mode\" in payload:\n                    self._attr_hvac_mode = payload[\"Mode\"].lower()\n                    # Some vendors send/receive mode as fan instead of fan_only\n                    if self._attr_hvac_mode == HVACAction.FAN:\n                        self._attr_hvac_mode = HVACMode.FAN_ONLY\n                if \"Temp\" in payload:\n                    if payload[\"Temp\"] > 0:\n                        if not (self.power_mode == STATE_OFF and self._ignore_off_temp):\n                            self._attr_target_temperature = payload[\"Temp\"]\n                if \"Celsius\" in payload:\n                    self._celsius = payload[\"Celsius\"].lower()\n                if \"Quiet\" in payload:\n                    self._quiet = payload[\"Quiet\"].lower()\n                if \"Turbo\" in payload:\n                    self._turbo = payload[\"Turbo\"].lower()\n                if \"Econo\" in payload:\n                    self._econo = payload[\"Econo\"].lower()\n                if \"Light\" in payload:\n                    self._light = payload[\"Light\"].lower()\n                if \"Filter\" in payload:\n                    self._filter = payload[\"Filter\"].lower()\n                if \"Clean\" in payload:\n                    self._clean = payload[\"Clean\"].lower()\n                if \"Beep\" in payload:\n                    self._beep = payload[\"Beep\"].lower()\n                if \"Sleep\" in payload:\n                    self._sleep = payload[\"Sleep\"]\n                if \"SwingV\" in payload:\n                    self._swingv = payload[\"SwingV\"].lower()\n                    if self._swingv != \"auto\":\n                        self._fix_swingv = self._swingv\n                if \"SwingH\" in payload:\n                    self._swingh = payload[\"SwingH\"].lower()\n                    if self._swingh != \"auto\":\n                        self._fix_swingh = self._swingh\n                if (\n                    \"SwingV\" in payload\n                    and payload[\"SwingV\"].lower() == STATE_AUTO\n                    and \"SwingH\" in payload\n                    and payload[\"SwingH\"].lower() == STATE_AUTO\n                ):\n                    if SWING_BOTH in (self._attr_swing_modes or []):\n                        self._attr_swing_mode = SWING_BOTH\n                    elif SWING_VERTICAL in (self._attr_swing_modes or []):\n                        self._attr_swing_mode = SWING_VERTICAL\n                    elif SWING_HORIZONTAL in (self._attr_swing_modes or []):\n                        self._attr_swing_mode = SWING_HORIZONTAL\n                    else:\n                        self._attr_swing_mode = SWING_OFF\n                elif (\n                    \"SwingV\" in payload\n                    and payload[\"SwingV\"].lower() == STATE_AUTO\n                    and SWING_VERTICAL in (self._attr_swing_modes or [])\n                ):\n                    self._attr_swing_mode = SWING_VERTICAL\n                elif (\n                    \"SwingH\" in payload\n                    and payload[\"SwingH\"].lower() == STATE_AUTO\n                    and SWING_HORIZONTAL in (self._attr_swing_modes or [])\n                ):\n                    self._attr_swing_mode = SWING_HORIZONTAL\n                else:\n                    self._attr_swing_mode = SWING_OFF\n\n                if \"FanSpeed\" in payload:\n                    fan_mode = payload[\"FanSpeed\"].lower()\n                    # ELECTRA_AC fan modes fix\n                    if self.use_electra_tweak:\n                        if fan_mode == HVAC_FAN_MAX:\n                            self._attr_fan_mode = FAN_HIGH\n                        elif fan_mode == HVAC_FAN_AUTO:\n                            self._attr_fan_mode = HVAC_FAN_MAX\n                        elif fan_mode == HVAC_FAN_MIN:\n                            self._attr_fan_mode = FAN_LOW\n                        else:\n                            self._attr_fan_mode = fan_mode\n                    else:\n                        self._attr_fan_mode = fan_mode\n                    _LOGGER.debug(self._attr_fan_mode)\n\n                if self._attr_hvac_mode != HVACMode.OFF:\n                    self._last_on_mode = self._attr_hvac_mode\n\n                # Set default state to off\n                if self.power_mode == STATE_OFF:\n                    self._attr_hvac_mode = HVACMode.OFF\n                    self._enabled = False\n                else:\n                    self._enabled = True\n\n                # Set toggles to 'off'\n                for key in self._toggle_list:\n                    setattr(self, \"_\" + key.lower(), \"off\")\n\n                # Update HA UI and State\n                self.async_schedule_update_ha_state()\n\n                # Check power sensor state\n                if (\n                    self._power_sensor\n                    and prev_power is not None\n                    and prev_power != self.power_mode\n                ):\n                    await asyncio.sleep(3)\n                    state = self.hass.states.get(self._power_sensor)\n                    # It's probably running in a special mode, such as an automatic cleaning function.\n                    is_special_mode = (\n                        True if state is not None and state.state else False\n                    )\n                    await self._async_power_sensor_changed(None, state, is_special_mode)\n\n        unsubscribe = []\n        unsubscribe.append(\n            await mqtt.async_subscribe(\n                self.hass, self.state_topic, state_message_received\n            )\n        )\n        unsubscribe.append(\n            await mqtt.async_subscribe(\n                self.hass, self.availability_topic, available_message_received\n            )\n        )\n        if self.state_topic2:\n            unsubscribe.append(\n                await mqtt.async_subscribe(\n                    self.hass, self.state_topic2, state_message_received\n                )\n            )\n\n        return unsubscribe\n\n    async def async_will_remove_from_hass(self):\n        \"\"\"Unsubscribe when removed.\"\"\"\n        for unsubscribe in self._unsubscribes:\n            unsubscribe()\n\n    @property\n    def precision(self):\n        \"\"\"Return the precision of the system.\"\"\"\n        if self._temp_precision is not None:\n            return self._temp_precision\n        return super().precision\n\n    # This extension property is written throughout the instance, so use @property instead of @cached_property.\n    @property\n    def hvac_action(self):\n        \"\"\"Return the current running hvac operation if supported.\n\n        Need to be one of CURRENT_HVAC_*.\n        \"\"\"\n        if self._attr_hvac_mode == HVACMode.OFF:\n            return HVACAction.OFF\n        elif self._attr_hvac_mode == HVACMode.HEAT:\n            return HVACAction.HEATING\n        elif self._attr_hvac_mode == HVACMode.COOL:\n            return HVACAction.COOLING\n        elif self._attr_hvac_mode == HVACMode.DRY:\n            return HVACAction.DRYING\n        elif self._attr_hvac_mode == HVACMode.FAN_ONLY:\n            return HVACAction.FAN\n\n    # This extension property is written throughout the instance, so use @property instead of @cached_property.\n    @property\n    def extra_state_attributes(self):\n        \"\"\"Return the state attributes of the device.\"\"\"\n        return {\n            attr: getattr(self, \"_\" + prop) for attr, prop in ATTRIBUTES_IRHVAC.items()\n        }\n\n    @property\n    def last_on_mode(self):\n        \"\"\"Return the last non-idle mode ie. heat, cool.\"\"\"\n        return self._last_on_mode\n\n    async def async_set_hvac_mode(self, hvac_mode):\n        \"\"\"Set hvac mode.\"\"\"\n        await self.set_mode(hvac_mode)\n        # Ensure we update the current operation after changing the mode\n        await self.async_send_cmd()\n\n    async def async_turn_on(self):\n        \"\"\"Turn thermostat on.\"\"\"\n        self._attr_hvac_mode = (\n            self._last_on_mode if self._last_on_mode is not None else HVACMode.AUTO\n        )\n        self.power_mode = STATE_ON\n        await self.async_send_cmd()\n\n    async def async_turn_off(self):\n        \"\"\"Turn thermostat off.\"\"\"\n        self._attr_hvac_mode = HVACMode.OFF\n        self.power_mode = STATE_OFF\n        await self.async_send_cmd()\n\n    async def async_set_temperature(self, **kwargs):\n        \"\"\"Set new target temperature.\"\"\"\n        temperature = kwargs.get(ATTR_TEMPERATURE)\n        hvac_mode = kwargs.get(ATTR_HVAC_MODE)\n        if temperature is None:\n            return\n\n        if hvac_mode is not None:\n            await self.set_mode(hvac_mode)\n\n        self._attr_target_temperature = temperature\n        if not self._attr_hvac_mode == HVACMode.OFF:\n            self.power_mode = STATE_ON\n        await self.async_send_cmd()\n\n    async def async_set_fan_mode(self, fan_mode):\n        \"\"\"Set new target fan mode.\"\"\"\n        if fan_mode not in (self._attr_fan_modes or []):\n            # tweak for some ELECTRA_AC devices\n            if self.use_electra_tweak:\n                if fan_mode != FAN_HIGH and fan_mode != HVAC_FAN_MAX:\n                    _LOGGER.error(\n                        \"Invalid swing mode selected. Got '%s'. Allowed modes are:\",\n                        fan_mode,\n                    )\n                    _LOGGER.error(self._attr_fan_modes)\n                    return\n            else:\n                _LOGGER.error(\n                    \"Invalid swing mode selected. Got '%s'. Allowed modes are:\",\n                    fan_mode,\n                )\n                _LOGGER.error(self._attr_fan_modes)\n                return\n\n        self._attr_fan_mode = fan_mode\n        if not self._attr_hvac_mode == HVACMode.OFF:\n            self.power_mode = STATE_ON\n        await self.async_send_cmd()\n\n    async def async_set_swing_mode(self, swing_mode):\n        \"\"\"Set new target swing operation.\"\"\"\n        if swing_mode not in (self._attr_swing_modes or []):\n            _LOGGER.error(\n                \"Invalid swing mode selected. Got '%s'. Allowed modes are:\", swing_mode\n            )\n            _LOGGER.error(self._attr_swing_modes)\n            return\n        self._attr_swing_mode = swing_mode\n        # note: set _swingv and _swingh in send_ir() later\n        if not self._attr_hvac_mode == HVACMode.OFF:\n            self.power_mode = STATE_ON\n        await self.async_send_cmd()\n\n    async def async_set_econo(self, econo, state_mode):\n        \"\"\"Set new target econo mode.\"\"\"\n        if econo not in ON_OFF_LIST:\n            return\n        self._econo = econo.lower()\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_turbo(self, turbo, state_mode):\n        \"\"\"Set new target turbo mode.\"\"\"\n        if turbo not in ON_OFF_LIST:\n            return\n        self._turbo = turbo.lower()\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_quiet(self, quiet, state_mode):\n        \"\"\"Set new target quiet mode.\"\"\"\n        if quiet not in ON_OFF_LIST:\n            return\n        self._quiet = quiet.lower()\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_light(self, light, state_mode):\n        \"\"\"Set new target light mode.\"\"\"\n        if light not in ON_OFF_LIST:\n            return\n        self._light = light.lower()\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_filters(self, filters, state_mode):\n        \"\"\"Set new target filters mode.\"\"\"\n        if filters not in ON_OFF_LIST:\n            return\n        self._filter = filters.lower()\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_clean(self, clean, state_mode):\n        \"\"\"Set new target clean mode.\"\"\"\n        if clean not in ON_OFF_LIST:\n            return\n        self._clean = clean.lower()\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_beep(self, beep, state_mode):\n        \"\"\"Set new target beep mode.\"\"\"\n        if beep not in ON_OFF_LIST:\n            return\n        self._beep = beep.lower()\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_sleep(self, sleep, state_mode):\n        \"\"\"Set new target sleep mode.\"\"\"\n        self._sleep = sleep.lower()\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_swingv(self, swingv, state_mode):\n        \"\"\"Set new target swingv.\"\"\"\n        self._swingv = swingv.lower()\n        if self._swingv != \"auto\":\n            self._fix_swingv = self._swingv\n            if self._attr_swing_mode == SWING_BOTH:\n                if SWING_HORIZONTAL in (self._attr_swing_modes or []):\n                    self._attr_swing_mode = SWING_HORIZONTAL\n            elif self._attr_swing_mode == SWING_VERTICAL:\n                self._attr_swing_mode = SWING_OFF\n        else:\n            if self._attr_swing_mode == SWING_HORIZONTAL:\n                if SWING_BOTH in (self._attr_swing_modes or []):\n                    self._attr_swing_mode = SWING_BOTH\n            else:\n                if SWING_VERTICAL in (self._attr_swing_modes or []):\n                    self._attr_swing_mode = SWING_VERTICAL\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_set_swingh(self, swingh, state_mode):\n        \"\"\"Set new target swingh.\"\"\"\n        self._swingh = swingh.lower()\n        if self._swingh != \"auto\":\n            self._fix_swingh = self._swingh\n            if self._attr_swing_mode == SWING_BOTH:\n                if SWING_VERTICAL in (self._attr_swing_modes or []):\n                    self._attr_swing_mode = SWING_VERTICAL\n            elif self._attr_swing_mode == SWING_HORIZONTAL:\n                self._attr_swing_mode = SWING_OFF\n        else:\n            if self._attr_swing_mode == SWING_VERTICAL:\n                if SWING_BOTH in (self._attr_swing_modes or []):\n                    self._attr_swing_mode = SWING_BOTH\n            else:\n                if SWING_HORIZONTAL in (self._attr_swing_modes or []):\n                    self._attr_swing_mode = SWING_HORIZONTAL\n        self._state_mode = state_mode\n        await self.async_send_cmd()\n\n    async def async_send_cmd(self):\n        await self.send_ir()\n\n    @cached_property\n    def min_temp(self):\n        \"\"\"Return the minimum temperature.\"\"\"\n        if self._min_temp:\n            return self._min_temp\n\n        # get default temp from super class\n        return super().min_temp\n\n    @cached_property\n    def max_temp(self):\n        \"\"\"Return the maximum temperature.\"\"\"\n        if self._max_temp:\n            return self._max_temp\n\n        # Get default temp from super class\n        return super().max_temp\n\n    async def _async_sensor_changed(\n        self, entity_id_or_event, old_state=None, new_state=None\n    ):\n        # Replacing `async_track_state_change` with `async_track_state_change_event`\n        # See, https://developers.home-assistant.io/blog/2024/04/13/deprecate_async_track_state_change/\n        if self._use_track_state_change_event:\n            entity_id = entity_id_or_event.data[\"entity_id\"]\n            old_state = entity_id_or_event.data[\"old_state\"]\n            new_state = entity_id_or_event.data[\"new_state\"]\n        else:\n            entity_id = entity_id_or_event\n\n        if new_state is None:\n            return\n\n        if entity_id == self._temp_sensor:\n            self._async_update_temp(new_state)\n            self.async_schedule_update_ha_state()\n        elif entity_id == self._humidity_sensor:\n            self._async_update_humidity(new_state)\n            self.async_schedule_update_ha_state()\n        elif entity_id == self._power_sensor:\n            await self._async_power_sensor_changed(old_state, new_state)\n\n    async def _async_power_sensor_changed(\n        self, old_state, new_state, is_special_mode=False\n    ):\n        \"\"\"Handle power sensor changes.\"\"\"\n        if new_state is None:\n            return\n\n        if old_state is not None and new_state.state == old_state.state:\n            return\n\n        if new_state.state == STATE_ON:\n            if self._attr_hvac_mode == HVACMode.OFF or self.power_mode == STATE_OFF:\n                self._attr_hvac_mode = (\n                    self._special_mode\n                    if self._special_mode and is_special_mode\n                    else self._last_on_mode\n                )\n                self.power_mode = STATE_ON\n                self.async_schedule_update_ha_state()\n\n        elif new_state.state == STATE_OFF:\n            if self._attr_hvac_mode != HVACMode.OFF or self.power_mode == STATE_ON:\n                self._attr_hvac_mode = HVACMode.OFF\n                self.power_mode = STATE_OFF\n                self.async_schedule_update_ha_state()\n\n    @callback\n    def _async_update_temp(self, state):\n        \"\"\"Update thermostat with latest state from sensor.\"\"\"\n        if state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):\n            return\n        try:\n            self._attr_current_temperature = TemperatureConverter.convert(\n                float(state.state),\n                state.attributes[\"unit_of_measurement\"],\n                self.temperature_unit,\n            )\n        except (ValueError, KeyError) as ex:\n            _LOGGER.error(\"Unable to update from sensor: %s\", ex)\n\n    @callback\n    def _async_update_humidity(self, state):\n        \"\"\"Update thermostat with latest state from humidity sensor.\"\"\"\n        try:\n            if state.state != STATE_UNKNOWN and state.state != STATE_UNAVAILABLE:\n                self._attr_current_humidity = int(float(state.state))\n        except ValueError as ex:\n            _LOGGER.error(\"Unable to update from humidity sensor: %s\", ex)\n\n    @cached_property\n    def supported_features(self):\n        \"\"\"Return the list of supported features.\"\"\"\n        return self._support_flags\n\n    async def async_set_preset_mode(self, preset_mode):\n        \"\"\"Set new preset mode.\n\n        This method must be run in the event loop and returns a coroutine.\n        \"\"\"\n        if preset_mode == PRESET_AWAY and not self._is_away:\n            self._is_away = True\n            self._saved_target_temp = self._attr_target_temperature\n            self._attr_target_temperature = self._away_temp\n        elif preset_mode == PRESET_NONE and self._is_away:\n            self._is_away = False\n            self._attr_target_temperature = self._saved_target_temp\n        self._attr_preset_mode = PRESET_AWAY if self._is_away else PRESET_NONE\n        await self.send_ir()\n\n    async def set_mode(self, hvac_mode):\n        \"\"\"Set hvac mode.\"\"\"\n        hvac_mode = hvac_mode.lower()\n        if hvac_mode not in self._attr_hvac_modes or hvac_mode == HVACMode.OFF:\n            self._attr_hvac_mode = HVACMode.OFF\n            self._enabled = False\n            self.power_mode = STATE_OFF\n        else:\n            self._attr_hvac_mode = self._last_on_mode = hvac_mode\n            self._enabled = True\n            self.power_mode = STATE_ON\n\n    async def send_ir(self):\n        \"\"\"Send the payload to tasmota mqtt topic.\"\"\"\n        fan_speed = self.fan_mode\n\n        # tweak for some ELECTRA_AC devices\n        if self.use_electra_tweak:\n            if self.fan_mode == FAN_HIGH:\n                fan_speed = HVAC_FAN_MAX\n            if self.fan_mode == HVAC_FAN_MAX:\n                fan_speed = HVAC_FAN_AUTO\n            if self.fan_mode == FAN_LOW:\n                fan_speed = HVAC_FAN_MIN\n\n        # Set the swing mode - default off\n        self._swingv = STATE_OFF if self._fix_swingv is None else self._fix_swingv\n        self._swingh = STATE_OFF if self._fix_swingh is None else self._fix_swingh\n\n        if SWING_BOTH in (self._attr_swing_modes or []) or SWING_VERTICAL in (\n            self._attr_swing_modes or []\n        ):\n            if (\n                self._attr_swing_mode == SWING_BOTH\n                or self._attr_swing_mode == SWING_VERTICAL\n            ):\n                self._swingv = STATE_AUTO\n\n        if SWING_BOTH in (self._attr_swing_modes or []) or SWING_HORIZONTAL in (\n            self._attr_swing_modes or []\n        ):\n            if (\n                self._attr_swing_mode == SWING_BOTH\n                or self._attr_swing_mode == SWING_HORIZONTAL\n            ):\n                self._swingh = STATE_AUTO\n\n        _dt = dt_util.now()\n        _min = _dt.hour * 60 + _dt.minute\n\n        # Populate the payload\n        payload_data = {\n            \"StateMode\": self._state_mode,\n            \"Vendor\": self._vendor,\n            \"Model\": self._model,\n            \"Power\": self.power_mode,\n            \"Mode\": self._last_on_mode if self._keep_mode else self._attr_hvac_mode,\n            \"Celsius\": self._celsius,\n            \"Temp\": round(self._attr_target_temperature / self._temp_precision)\n            * self._temp_precision,\n            \"FanSpeed\": fan_speed,\n            \"SwingV\": self._swingv,\n            \"SwingH\": self._swingh,\n            \"Quiet\": self._quiet,\n            \"Turbo\": self._turbo,\n            \"Econo\": self._econo,\n            \"Light\": self._light,\n            \"Filter\": self._filter,\n            \"Clean\": self._clean,\n            \"Beep\": self._beep,\n            \"Sleep\": self._sleep,\n            \"Clock\": int(_min),\n            \"Weekday\": int(_dt.weekday()),\n        }\n        self._state_mode = DEFAULT_STATE_MODE\n        for key in self._toggle_list:\n            setattr(self, \"_\" + key.lower(), \"off\")\n\n        payload = json.dumps(payload_data)\n\n        # Publish mqtt message\n        if float(self._mqtt_delay) != float(DEFAULT_MQTT_DELAY):\n            await asyncio.sleep(float(self._mqtt_delay))\n\n        await mqtt.async_publish(self.hass, self.topic, payload)\n\n        # Update HA UI and State\n        self.async_schedule_update_ha_state()\n"
  },
  {
    "path": "custom_components/tasmota_irhvac/const.py",
    "content": "\"\"\"Provides the constants needed for component.\"\"\"\n\nfrom homeassistant.components.climate.const import HVACMode\n\n# States\nSTATE_AUTO = \"auto\"\nSTATE_COOL = \"cool\"\nSTATE_DRY = \"dry\"\nSTATE_FAN_ONLY = \"fan_only\"\nSTATE_HEAT = \"heat\"\n\n# Fan speeds\nHVAC_FAN_AUTO = \"auto\"\nHVAC_FAN_MIN = \"min\"\nHVAC_FAN_MEDIUM = \"medium\"\nHVAC_FAN_MAX = \"max\"\n\n# Some devices have \"auto\" and \"fan_only\" changed\nHVAC_MODE_AUTO_FAN = \"auto_fan_only\"\n\n# Some devicec have \"fan_only\" and \"auto\" changed\nHVAC_MODE_FAN_AUTO = \"fan_only_auto\"\n\n# Some devices say max,but it is high, and auto which is max\nHVAC_FAN_MAX_HIGH = \"max_high\"\nHVAC_FAN_AUTO_MAX = \"auto_max\"\n\n# Hvac moed list\nHVAC_MODES = [\n    HVACMode.OFF,\n    HVACMode.HEAT,\n    HVACMode.COOL,\n    HVACMode.HEAT_COOL,\n    HVACMode.AUTO,\n    HVACMode.DRY,\n    HVACMode.FAN_ONLY,\n    HVAC_MODE_AUTO_FAN,\n    HVAC_MODE_FAN_AUTO,\n]\n\n# Platform specific config entry names\nCONF_EXCLUSIVE_GROUP_VENDOR = \"exclusive_group_vendor\"\nCONF_VENDOR = \"vendor\"\nCONF_PROTOCOL = \"protocol\"  # Soon to be deprecated\nCONF_COMMAND_TOPIC = \"command_topic\"\nCONF_STATE_TOPIC = \"state_topic\"\nCONF_AVAILABILITY_TOPIC = \"availability_topic\"\nCONF_TEMP_SENSOR = \"temperature_sensor\"\nCONF_HUMIDITY_SENSOR = \"humidity_sensor\"\nCONF_POWER_SENSOR = \"power_sensor\"\nCONF_MQTT_DELAY = \"mqtt_delay\"\nCONF_MIN_TEMP = \"min_temp\"\nCONF_MAX_TEMP = \"max_temp\"\nCONF_TARGET_TEMP = \"target_temp\"\nCONF_INITIAL_OPERATION_MODE = \"initial_operation_mode\"\nCONF_AWAY_TEMP = \"away_temp\"\nCONF_PRECISION = \"precision\"\nCONF_TEMP_STEP = \"temp_step\"\nCONF_MODES_LIST = \"supported_modes\"\nCONF_FAN_LIST = \"supported_fan_speeds\"\nCONF_SWING_LIST = \"supported_swing_list\"\nCONF_QUIET = \"default_quiet_mode\"\nCONF_TURBO = \"default_turbo_mode\"\nCONF_ECONO = \"default_econo_mode\"\nCONF_MODEL = \"hvac_model\"\nCONF_CELSIUS = \"celsius_mode\"\nCONF_LIGHT = \"default_light_mode\"\nCONF_FILTER = \"default_filter_mode\"\nCONF_CLEAN = \"default_clean_mode\"\nCONF_BEEP = \"default_beep_mode\"\nCONF_SLEEP = \"default_sleep_mode\"\nCONF_KEEP_MODE = \"keep_mode_when_off\"\nCONF_SWINGV = \"default_swingv\"\nCONF_SWINGH = \"default_swingh\"\nCONF_TOGGLE_LIST = \"toggle_list\"\nCONF_IGNORE_OFF_TEMP = \"ignore_off_temp\"\nCONF_SPECIAL_MODE = \"special_mode\"\n\n# Platform specific default values\nDEFAULT_NAME = \"IR AirConditioner\"\nDEFAULT_STATE_TOPIC = \"state\"\nDEFAULT_COMMAND_TOPIC = \"topic\"\nDEFAULT_MQTT_DELAY = 0\nDEFAULT_TARGET_TEMP = 26\nDEFAULT_MIN_TEMP = 16\nDEFAULT_MAX_TEMP = 32\nDEFAULT_PRECISION = 1\nDEFAULT_FAN_LIST = [HVAC_FAN_AUTO_MAX, HVAC_FAN_MAX_HIGH, HVAC_FAN_MEDIUM, HVAC_FAN_MIN]\nDEFAULT_CONF_QUIET = \"off\"\nDEFAULT_CONF_TURBO = \"off\"\nDEFAULT_CONF_ECONO = \"off\"\nDEFAULT_CONF_MODEL = \"-1\"\nDEFAULT_CONF_CELSIUS = \"on\"\nDEFAULT_CONF_LIGHT = \"off\"\nDEFAULT_CONF_FILTER = \"off\"\nDEFAULT_CONF_CLEAN = \"off\"\nDEFAULT_CONF_BEEP = \"off\"\nDEFAULT_CONF_SLEEP = \"-1\"\nDEFAULT_CONF_KEEP_MODE = False\nDEFAULT_STATE_MODE = \"SendStore\"\nDEFAULT_IGNORE_OFF_TEMP = False\n\nATTR_NAME = \"name\"\nATTR_VALUE = \"value\"\n\nDATA_KEY = \"tasmota_irhvac.climate\"\nDOMAIN = \"tasmota_irhvac\"\n\nATTR_ECONO = \"econo\"\nATTR_TURBO = \"turbo\"\nATTR_QUIET = \"quiet\"\nATTR_LIGHT = \"light\"\nATTR_FILTERS = \"filters\"\nATTR_CLEAN = \"clean\"\nATTR_BEEP = \"beep\"\nATTR_SLEEP = \"sleep\"\nATTR_LAST_ON_MODE = \"last_on_mode\"\nATTR_SWINGV = \"swingv\"\nATTR_SWINGH = \"swingh\"\nATTR_FIX_SWINGV = \"fix_swingv\"\nATTR_FIX_SWINGH = \"fix_swingh\"\nATTR_STATE_MODE = \"state_mode\"\n\nSERVICE_ECONO_MODE = \"set_econo\"\nSERVICE_TURBO_MODE = \"set_turbo\"\nSERVICE_QUIET_MODE = \"set_quiet\"\nSERVICE_LIGHT_MODE = \"set_light\"\nSERVICE_FILTERS_MODE = \"set_filters\"\nSERVICE_CLEAN_MODE = \"set_clean\"\nSERVICE_BEEP_MODE = \"set_beep\"\nSERVICE_SLEEP_MODE = \"set_sleep\"\nSERVICE_SET_SWINGV = \"set_swingv\"\nSERVICE_SET_SWINGH = \"set_swingh\"\n\n# Map attributes to properties of the state object\nATTRIBUTES_IRHVAC = {\n    ATTR_ECONO: \"econo\",\n    ATTR_TURBO: \"turbo\",\n    ATTR_QUIET: \"quiet\",\n    ATTR_LIGHT: \"light\",\n    ATTR_FILTERS: \"filter\",\n    ATTR_CLEAN: \"clean\",\n    ATTR_BEEP: \"beep\",\n    ATTR_SLEEP: \"sleep\",\n    ATTR_LAST_ON_MODE: \"last_on_mode\",\n    ATTR_SWINGV: \"swingv\",\n    ATTR_SWINGH: \"swingh\",\n    ATTR_FIX_SWINGV: \"fix_swingv\",\n    ATTR_FIX_SWINGH: \"fix_swingh\",\n}\n\nON_OFF_LIST = [\"ON\", \"OFF\", \"On\", \"Off\", \"on\", \"off\"]\n\nTOGGLE_ALL_LIST = [\n    \"SwingV\",\n    \"SwingH\",\n    \"Quiet\",\n    \"Turbo\",\n    \"Econo\",\n    \"Light\",\n    \"Filter\",\n    \"Clean\",\n    \"Beep\",\n    \"Sleep\",\n]\n\nSTATE_MODE_LIST = [\"StoreOnly\", \"SendStore\"]\n"
  },
  {
    "path": "custom_components/tasmota_irhvac/manifest.json",
    "content": "{\n  \"domain\": \"tasmota_irhvac\",\n  \"name\": \"Tasmota Irhvac\",\n  \"version\": \"2026.4.5\",\n  \"documentation\": \"https://github.com/hristo-atanasov/Tasmota-IRHVAC\",\n  \"issue_tracker\": \"https://github.com/hristo-atanasov/Tasmota-IRHVAC/issues\",\n  \"homeassistant\": \"2024.11.0\",\n  \"requirements\": [],\n  \"dependencies\": [\n    \"mqtt\",\n    \"sensor\"\n  ],\n  \"codeowners\": [\n    \"@hristo-atanasov\",\n    \"@nao-pon\"\n  ]\n}\n"
  },
  {
    "path": "custom_components/tasmota_irhvac/services.yaml",
    "content": "set_econo:\n  description: Sets Econo mode.\n  target:\n    entity:\n      integration: tasmota_irhvac\n  fields:\n    econo:\n      description: Sets Econo mode\n      example: \"on\"\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"on\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_turbo:\n  description: Sets Turbo mode.\n  target:\n    entity:\n      integration: tasmota_irhvac\n  fields:\n    turbo:\n      description: Sets Turbo mode\n      example: \"on\"\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"on\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_filters:\n  description: Sets Filters mode.\n  target:\n    entity:\n      integration: tasmota_irhvac\n  fields:\n    filters:\n      description: Sets Filters mode\n      example: \"on\"\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"on\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_light:\n  target:\n    entity:\n      integration: tasmota_irhvac\n  description: Sets Light mode.\n  fields:\n    light:\n      description: Sets Light mode\n      example: \"on\"\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"on\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_quiet:\n  target:\n    entity:\n      integration: tasmota_irhvac\n  description: Sets Quiet mode.\n  fields:\n    quiet:\n      description: Sets Quiet mode\n      example: \"on\"\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"on\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_clean:\n  target:\n    entity:\n      integration: tasmota_irhvac\n  description: Sets Clean mode.\n  fields:\n    clean:\n      description: Sets Clean mode\n      example: \"on\"\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"on\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_beep:\n  target:\n    entity:\n      integration: tasmota_irhvac\n  description: Sets Beep mode.\n  fields:\n    beep:\n      description: Sets Beep mode\n      example: \"on\"\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"on\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_sleep:\n  description: Sets Sleep mode.\n  target:\n    entity:\n      integration: tasmota_irhvac\n  fields:\n    sleep:\n      description: Sets Sleep mode\n      example: \"0\"\n      required: true\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_swingv:\n  description: Sets vane vertical position.\n  target:\n    entity:\n      integration: tasmota_irhvac\n  fields:\n    swingv:\n      description: '\"off\", \"auto\", \"highest\", \"high\", \"middle\", \"low\" or \"lowest\", but only those supported by this model.'\n      example: '\"middle\"'\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"auto\"\n            - \"highest\"\n            - \"high\"\n            - \"middle\"\n            - \"low\"\n            - \"lowest\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n\nset_swingh:\n  name: Set swingh\n  description: Sets vane horizonal position.\n  target:\n    entity:\n      integration: tasmota_irhvac\n  fields:\n    swingh:\n      description: '\"off\", \"auto\", \"left max\", \"left\", \"middle\", \"right\", \"right max\" or \"wide\", but only those supported by this model.'\n      example: '\"middle\"'\n      required: true\n      selector:\n        select:\n          options:\n            - \"off\"\n            - \"auto\"\n            - \"left max\"\n            - \"left\"\n            - \"middle\"\n            - \"right\"\n            - \"right max\"\n            - \"wide\"\n    state_mode:\n      description: Sets StateMode in MQTT message. Default is \"SendStore\".\n      example: \"StoreOnly\"\n      required: false\n      selector:\n        select:\n          options:\n            - StoreOnly\n            - SendStore\n"
  },
  {
    "path": "examples/card_configuration.yaml",
    "content": "cards:\n  - cards:\n      - action: service\n        color: white\n        icon: \"mdi:power\"\n        name: Turn On Audio HEX\n        service:\n          action: ir_code\n          data:\n            bits: 12\n            data: A80\n            protocol: SONY\n            room: kitchen\n          domain: script\n        style:\n          - color: white\n          - background: green\n          - \"--disabled-text-color\": white\n        type: \"custom:button-card\"\n      - action: service\n        color: white\n        icon: \"mdi:power\"\n        name: Turn Off Audio HEX\n        service:\n          action: ir_code\n          data:\n            bits: 12\n            data: E85\n            protocol: SONY\n            room: kitchen\n          domain: script\n        style:\n          - color: white\n          - background: red\n          - \"--disabled-text-color\": white\n        type: \"custom:button-card\"\n      - action: service\n        color: white\n        icon: \"mdi:power\"\n        name: Test AC Raw\n        service:\n          action: ir_raw\n          data:\n            data: >-\n              3290, 1602,  424, 390,  424, 390,  424, 1232,  398, 390,  424,\n              1212,  420, 390,  424, 390,  424, 390,  424, 1232,  398, 1234, \n              398, 390,  424, 390,  426, 390,  424, 1232,  400, 1230,  398,\n              392,  424, 390,  426, 390,  426, 390,  424, 390,  424, 390,  424,\n              390,  424, 392,  424, 390,  424, 392,  424, 390,  424, 390,  424,\n              390,  424, 1232,  398, 390,  424, 390,  426, 390,  424, 390,  424,\n              392,  424, 390,  424, 392,  426, 1230,  400, 390,  424, 390,  426,\n              390,  424, 390,  424, 1232,  400, 1232,  398, 1232,  398, 1232, \n              400, 1232,  398, 1232,  400, 1232,  400, 1232,  400, 390,  426,\n              390,  424, 1206,  424, 390,  424, 390,  424, 392,  424, 390,  424,\n              392,  424, 390,  426, 390,  424, 390,  424, 1230,  402, 1230, \n              402, 390,  424, 390,  424, 1230,  402, 390,  424, 390,  424, 390, \n              424, 390,  424, 390,  426, 390,  424, 1230,  402, 1228,  402,\n              390,  424, 390,  424, 390,  426, 390,  424, 390,  426, 390,  424,\n              390,  424, 390,  426, 390,  426, 390,  424, 390,  424, 390,  426,\n              390,  424, 390,  424, 392,  426, 390,  424, 390,  424, 392,  424,\n              390,  424, 390,  424, 390,  424, 390,  424, 390,  424, 390,  424,\n              390,  424, 390,  426, 390,  426, 390,  424, 390,  424, 392,  424,\n              390,  424, 390,  424, 390,  424, 390,  424, 392,  424, 390,  424,\n              390,  424, 390,  426, 390,  424, 392,  424, 390,  424, 392,  424,\n              390,  424, 390,  424, 1228,  404, 388,  424, 390,  424, 392,  424,\n              1228,  404, 1228,  402, 1228,  402, 390,  426, 1228,  402, 390, \n              424, 390,  424\n            room: bedroom\n          domain: script\n        style:\n          - color: white\n          - background: blue\n          - \"--disabled-text-color\": white\n        type: \"custom:button-card\"\n    type: vertical-stack\ntype: vertical-stack\n"
  },
  {
    "path": "examples/configuration.yaml",
    "content": "climate:\n  - platform: tasmota_irhvac\n    name: \"Some Name Here\"\n    command_topic: \"cmnd/your_tasmota_device/irhvac\"\n    # Pick one of the following:\n    # State is updated when the tasmota device receives an IR signal (includes own transmission and original remote)\n    # useful when a normal remote is in use alongside the tasmota device, may be less reliable than the second option.\n    state_topic: \"tele/your_tasmota_device/RESULT\"\n    # State is updated when the tasmota device completes IR transmissionm, should be pretty reliable.\n    #state_topic: \"stat/your_tasmota_device/RESULT\"\n    # Optional second state topic, This option allows you to subscribe to both \"tele\" and \"stat\" messages.\n    state_topic_2: \"stat/your_tasmota_device/RESULT\"\n    # Uncomment if your 'available topic' of Tasmota IR device are different (if device in HA is disabled)\n    #availability_topic: \"tele/your_tasmota_device/LWT\"\n    temperature_sensor: sensor.kitchen_temperature\n    humidity_sensor: sensor.kitchen_humidity #optional - default None\n    power_sensor: binaly_sensor.kitchen_ac_power #optional - default None\n    vendor: \"ELECTRA_AC\"\n    # When operating grouped devices at the same time, MQTT commands are intentionally delayed to prevent multiple devices\n    # from performing the same operation at the same time. This allows the high current peaks to be shifted.\n    mqtt_delay: 0.0 #optional - default 0 int or 0.0 float value in [sec].\n    min_temp: 16 #optional - default 16 int value\n    max_temp: 32 #optional - default 32 int value\n    target_temp: 26 #optional - default 26 int value\n    initial_operation_mode: \"off\" # optional - default \"off\" string value (one of the \"supported_modes\")\n    away_temp: 24 #optional - default 24 int value\n    precision: 1 #optional - default 1 int or float value. Can be set to 1, 0.5 or 0.1\n    supported_modes:\n      - \"heat\"\n      - \"cool\"\n      - \"dry\"\n      - \"fan_only\" # Use \"fan_only\" even if Tasmota shows \"Mode\":\"Fan\"\n      - \"auto\"\n      - \"off\" #Turns the AC off - Should be in quotes\n      # Some devices have \"auto\" and \"fan_only\" switched\n      # If the following two lines are uncommented, \"auto\" and \"fan\" shoud be commented out\n      #- \"auto_fan_only\" #if remote shows fan but tasmota says auto\n      #- \"fan_only_auto\" #if remote shows auto but tasmota says fan\n    supported_fan_speeds:\n      # Some devices say max,but it is high, and auto which is max\n      # If you uncomment the following two, you have to comment high and max\n      # - \"auto_max\" #woud become max\n      # - \"max_high\" #would become high\n      #- \"on\"\n      #- \"off\"\n      #- \"low\"\n      - \"medium\"\n      - \"high\"\n      #- \"middle\"\n      #- \"focus\"\n      #- \"diffuse\"\n      #- \"top\"\n      - \"min\"\n      - \"max\"\n      #- \"auto\"\n    supported_swing_list:\n      - \"off\"\n      - \"vertical\" #up to down\n      # - \"horizontal\" # Left to right\n      # - \"both\"\n    default_quiet_mode: \"Off\" #optional - default \"Off\" string value\n    default_turbo_mode: \"Off\" #optional - default \"Off\" string value\n    default_econo_mode: \"Off\" #optional - default \"Off\" string value\n    hvac_model: \"-1\" #optional - default \"1\" string value\n    celsius_mode: \"On\" #optional - default \"On\" string value\n    default_light_mode: \"Off\" #optional - default \"Off\" string value\n    default_filter_mode: \"Off\" #optional - default \"Off\" string value\n    default_clean_mode: \"Off\" #optional - default \"Off\" string value\n    default_beep_mode: \"Off\" #optional - default \"Off\" string value\n    default_sleep_mode: \"-1\" #optional - default \"-1\" string value\n    default_swingv: \"high\" #optional - default \"\" string value\n    default_swingh: \"left\" #optional - default \"\" string value \n    keep_mode_when_off: True #optional - default False boolean value : Must be True for MITSUBISHI_AC, ECOCLIM, etc.\n    # toggle_list: #optional - default []\n      # The toggled property is a setting that does not retain the On state.\n      # Set this if your AC properties are toggle function.\n      #- Beep\n      #- Clean\n      #- Econo\n      #- Filter\n      #- Light\n      #- Quiet\n      #- Sleep\n      #- SwingH\n      #- SwingV\n      #- Turbo\n    # When turning off some devices with their remote control they are set to the lowest temperature\n    # 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.\n    ignore_off_temp: False #optional - default False boolean value\n    # Some air conditioners have a function to enter special modes such as cleaning\n    # mode after operation. This mode detects and sets this.\n    # \"auto\", \"cool\", \"dry\", \"fan_only\", \"heat\" or \"off\"\n    special_mode: \"\" #optional - default \"\" is current mode string value\n"
  },
  {
    "path": "examples/scripts.yaml",
    "content": "ir_code:\n  sequence:\n    - data_template:\n        payload: '{\"Protocol\":\"{{ protocol }}\",\"Bits\": {{ bits }},\"Data\": 0x{{ data }}}'\n        topic: \"cmnd/{{ room }}Multisensor/irsend\"\n      service: mqtt.publish\nir_raw:\n  sequence:\n    - data_template:\n        payload: \"0, {{ data }}\"\n        topic: \"cmnd/{{ room }}Multisensor/irsend\"\n      service: mqtt.publish\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n  \"name\": \"Tasmota-IRHVAC\",\n  \"render_readme\": true,\n  \"homeassistant\": \"2024.11.0\"\n}\n"
  }
]