Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions custom_components/smartir/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from . import COMPONENT_ABS_DIR, Helper
from .controller import get_controller
from .controller import MQTT_CONTROLLER, get_controller

_LOGGER = logging.getLogger(__name__)

Expand All @@ -32,6 +32,8 @@
CONF_HUMIDITY_SENSOR = 'humidity_sensor'
CONF_POWER_SENSOR = 'power_sensor'
CONF_POWER_SENSOR_RESTORE_STATE = 'power_sensor_restore_state'
CONF_SEND_ON_COMMAND = 'send_on_command'
DEVICE_SEND_ON_COMMAND = 'sendOnCommand'

SUPPORT_FLAGS = (
ClimateEntityFeature.TURN_OFF |
Expand All @@ -49,7 +51,8 @@
vol.Optional(CONF_TEMPERATURE_SENSOR): cv.entity_id,
vol.Optional(CONF_HUMIDITY_SENSOR): cv.entity_id,
vol.Optional(CONF_POWER_SENSOR): cv.entity_id,
vol.Optional(CONF_POWER_SENSOR_RESTORE_STATE, default=False): cv.boolean
vol.Optional(CONF_POWER_SENSOR_RESTORE_STATE, default=False): cv.boolean,
vol.Optional(CONF_SEND_ON_COMMAND): cv.boolean,
})

async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
Expand Down Expand Up @@ -109,6 +112,7 @@ def __init__(self, hass, config, device_data):
self._humidity_sensor = config.get(CONF_HUMIDITY_SENSOR)
self._power_sensor = config.get(CONF_POWER_SENSOR)
self._power_sensor_restore_state = config.get(CONF_POWER_SENSOR_RESTORE_STATE)
self._send_on_command = self._resolve_send_on_command(config, device_data)

self._manufacturer = device_data['manufacturer']
self._supported_models = device_data['supportedModels']
Expand Down Expand Up @@ -155,7 +159,24 @@ def __init__(self, hass, config, device_data):
self._commands_encoding,
self._controller_data,
self._delay)


@staticmethod
def _resolve_send_on_command(config, device_data):
"""Resolve whether to send a separate power-on IR packet before state commands.

Priority: YAML ``send_on_command`` > device JSON ``sendOnCommand`` >
controller default (``False`` for MQTT, ``True`` otherwise).

Stateful AC codes (mode + fan + temperature in one packet) break when a
generic ``on`` command is sent first — common with Zigbee2MQTT blasters
(Moes UFO-R11, ZS06, etc.).
"""
if CONF_SEND_ON_COMMAND in config:
return config[CONF_SEND_ON_COMMAND]
if DEVICE_SEND_ON_COMMAND in device_data:
return device_data[DEVICE_SEND_ON_COMMAND]
return device_data['supportedController'] != MQTT_CONTROLLER

async def async_added_to_hass(self):
"""Run when entity about to be added."""
await super().async_added_to_hass()
Expand Down Expand Up @@ -293,7 +314,8 @@ def extra_state_attributes(self):
'manufacturer': self._manufacturer,
'supported_models': self._supported_models,
'supported_controller': self._supported_controller,
'commands_encoding': self._commands_encoding
'commands_encoding': self._commands_encoding,
'send_on_command': self._send_on_command,
}

async def async_set_temperature(self, **kwargs):
Expand Down Expand Up @@ -372,7 +394,7 @@ async def send_command(self):
await self._controller.send(self._commands['off'])
return

if 'on' in self._commands:
if 'on' in self._commands and self._send_on_command:
await self._controller.send(self._commands['on'])
await asyncio.sleep(self._delay)

Expand Down
23 changes: 22 additions & 1 deletion custom_components/smartir/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,33 @@ async def send(self, command):
"""Send a command."""
service_data = {
'topic': self._controller_data,
'payload': command
'payload': self._format_payload(command),
}

await self.hass.services.async_call(
'mqtt', 'publish', service_data)

def _format_payload(self, command):
"""Build an MQTT payload for Zigbee2MQTT and similar IR blasters.

- Topic ending in ``ir_code_to_send``: raw code string (Z2M convention).
- Command already JSON (e.g. ``{"ir_code_to_send":"..."}``): use as-is.
- Otherwise: wrap as ``{"ir_code_to_send": "<code>"}`` on the device
``set`` topic.
"""
if isinstance(command, dict):
return json.dumps(command)

command = command.strip()
if command.startswith('{'):
return command

topic = self._controller_data.rstrip('/')
if topic.endswith('ir_code_to_send'):
return command

return json.dumps({'ir_code_to_send': command})


class LookinController(AbstractController):
"""Controls a Lookin device."""
Expand Down
6 changes: 3 additions & 3 deletions custom_components/smartir/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
"codeowners": ["@smartHomeHub"],
"requirements": ["aiofiles>=0.6.0"],
"homeassistant": "2025.5.0",
"version": "1.18.1",
"version": "1.18.2",
"updater": {
"version": "1.18.1",
"releaseNotes": "-- Implements new async_track_state_change_event",
"version": "1.18.2",
"releaseNotes": "-- MQTT: auto payload format for Zigbee2MQTT; optional send_on_command for stateful AC",
"files": [
"__init__.py",
"climate.py",
Expand Down
27 changes: 26 additions & 1 deletion docs/CLIMATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ _Please note that the device_code field only accepts positive numbers. The .json
| `humidity_sensor` | string | optional | *entity_id* for a humidity sensor |
| `power_sensor` | string | optional | *entity_id* for a sensor that monitors whether your device is actually `on` or `off`. This may be a power monitor sensor. (Accepts only on/off states) |
| `power_sensor_restore_state` | boolean | optional | If `power_sensor` is set, and the device is likely to turn off and back on while still in the set mode (for instance, a minisplit cycling on and off while in heating or cooling mode), setting this to `true` will cause the climate state to update dynamically, following the state of the `power_sensor`. |
| `send_on_command` | boolean | optional | Send the separate `on` IR packet before mode/fan/temperature commands. Defaults to `false` for MQTT controllers and `true` for all others. Override when your AC needs (or must not use) a dedicated power-on code. Can also be set per device JSON as `sendOnCommand`. |

## Example (using broadlink controller):
Add a Broadlink RM device named "Bedroom" via config flow (read the [docs](https://www.home-assistant.io/integrations/broadlink/)).
Expand Down Expand Up @@ -57,6 +58,17 @@ climate:
```

## Example (using mqtt controller):
Use with any MQTT IR blaster. For **Zigbee2MQTT** devices (Moes UFO-R11, ZS06, etc.) set `supportedController` to `MQTT` and `commandsEncoding` to `Raw` in your device JSON.

`controller_data` is the MQTT topic passed to `mqtt.publish`. SmartIR picks the payload format automatically:

| `controller_data` topic | MQTT payload |
| ----------------------- | ------------ |
| `zigbee2mqtt/<device>/set/ir_code_to_send` | Raw IR code string |
| `zigbee2mqtt/<device>/set` | `{"ir_code_to_send":"<code>"}` |

Stateful split AC codes already include power/mode/fan/temperature. A separate `on` command before each state change breaks many MQTT blasters — SmartIR skips it by default for MQTT (`send_on_command: false`). Set `send_on_command: true` only if your AC requires a dedicated power-on packet (typical for some Broadlink setups).

```yaml
smartir:

Expand All @@ -65,13 +77,26 @@ climate:
name: Office AC
unique_id: office_ac
device_code: 3000
controller_data: home-assistant/office-ac/command
# Z2M: publish raw code to the ir_code_to_send sub-topic
controller_data: zigbee2mqtt/office_ir/set/ir_code_to_send
temperature_sensor: sensor.temperature
humidity_sensor: sensor.humidity
power_sensor: binary_sensor.ac_power
power_sensor_restore_state: true
```

Alternative Z2M topic (JSON payload on the device `set` topic):

```yaml
controller_data: zigbee2mqtt/office_ir/set
```

Explicit override when the default does not match your hardware:

```yaml
send_on_command: true
```

## Example (using LOOKin controller):
```yaml
smartir:
Expand Down