diff --git a/custom_components/smartir/climate.py b/custom_components/smartir/climate.py index f59fe46b0..fb85699b1 100644 --- a/custom_components/smartir/climate.py +++ b/custom_components/smartir/climate.py @@ -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__) @@ -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 | @@ -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): @@ -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'] @@ -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() @@ -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): @@ -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) diff --git a/custom_components/smartir/controller.py b/custom_components/smartir/controller.py index 30758f62b..bc54b153e 100644 --- a/custom_components/smartir/controller.py +++ b/custom_components/smartir/controller.py @@ -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": ""}`` 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.""" diff --git a/custom_components/smartir/manifest.json b/custom_components/smartir/manifest.json index f8a1a8f26..9c96b7216 100644 --- a/custom_components/smartir/manifest.json +++ b/custom_components/smartir/manifest.json @@ -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", diff --git a/docs/CLIMATE.md b/docs/CLIMATE.md index e7eeb9d49..1cc8269c1 100644 --- a/docs/CLIMATE.md +++ b/docs/CLIMATE.md @@ -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/)). @@ -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//set/ir_code_to_send` | Raw IR code string | +| `zigbee2mqtt//set` | `{"ir_code_to_send":""}` | + +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: @@ -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: