diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 999788a25e9..a083e12dd31 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -338,18 +338,53 @@ def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: return data -def publish(hass: HomeAssistant, topic, payload, qos=0, retain=False) -> None: - """Publish message to an MQTT topic.""" - hass.add_job(async_publish, hass, topic, payload, qos, retain) +def publish( + hass: HomeAssistant, + topic: str, + payload: PublishPayloadType, + qos: int | None = 0, + retain: bool | None = False, + encoding: str | None = DEFAULT_ENCODING, +) -> None: + """Publish message to a MQTT topic.""" + hass.add_job(async_publish, hass, topic, payload, qos, retain, encoding) async def async_publish( - hass: HomeAssistant, topic: Any, payload, qos=0, retain=False + hass: HomeAssistant, + topic: str, + payload: PublishPayloadType, + qos: int | None = 0, + retain: bool | None = False, + encoding: str | None = DEFAULT_ENCODING, ) -> None: - """Publish message to an MQTT topic.""" - await hass.data[DATA_MQTT].async_publish( - topic, str(payload) if not isinstance(payload, bytes) else payload, qos, retain - ) + """Publish message to a MQTT topic.""" + + outgoing_payload = payload + if not isinstance(payload, bytes): + if not encoding: + _LOGGER.error( + "Can't pass-through payload for publishing %s on %s with no encoding set, need 'bytes' got %s", + payload, + topic, + type(payload), + ) + return + outgoing_payload = str(payload) + if encoding != DEFAULT_ENCODING: + # a string is encoded as utf-8 by default, other encoding requires bytes as payload + try: + outgoing_payload = outgoing_payload.encode(encoding) + except (AttributeError, LookupError, UnicodeEncodeError): + _LOGGER.error( + "Can't encode payload for publishing %s on %s with encoding %s", + payload, + topic, + encoding, + ) + return + + await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) AsyncDeprecatedMessageCallbackType = Callable[ diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 4b28c994da3..c90875010b2 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -40,7 +40,14 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, +) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -326,6 +333,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) def _validate_code(self, code, state): diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 0ede705cb29..982e34d370d 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -17,7 +17,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN +from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN, DOMAIN from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper CONF_PAYLOAD_PRESS = "payload_press" @@ -100,4 +100,5 @@ class MqttButton(MqttEntity, ButtonEntity): self._config[CONF_PAYLOAD_PRESS], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 928101f8dc1..1c0a2ac2a53 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -58,7 +58,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import MQTT_BASE_PLATFORM_SCHEMA, PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt -from .const import CONF_QOS, CONF_RETAIN, DOMAIN +from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -685,6 +685,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def _set_temperature( diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index fad92d0cf4e..6d3701f8ebc 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -42,7 +42,14 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, +) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -547,6 +554,7 @@ class MqttCover(MqttEntity, CoverEntity): self._config[CONF_PAYLOAD_OPEN], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: # Optimistically assume that cover has changed state. @@ -568,6 +576,7 @@ class MqttCover(MqttEntity, CoverEntity): self._config[CONF_PAYLOAD_CLOSE], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: # Optimistically assume that cover has changed state. @@ -589,6 +598,7 @@ class MqttCover(MqttEntity, CoverEntity): self._config[CONF_PAYLOAD_STOP], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def async_open_cover_tilt(self, **kwargs): @@ -599,6 +609,7 @@ class MqttCover(MqttEntity, CoverEntity): self._config[CONF_TILT_OPEN_POSITION], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._tilt_optimistic: self._tilt_value = self.find_percentage_in_range( @@ -614,6 +625,7 @@ class MqttCover(MqttEntity, CoverEntity): self._config[CONF_TILT_CLOSED_POSITION], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._tilt_optimistic: self._tilt_value = self.find_percentage_in_range( @@ -643,6 +655,7 @@ class MqttCover(MqttEntity, CoverEntity): tilt, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._tilt_optimistic: _LOGGER.debug("Set tilt value optimistic") @@ -670,6 +683,7 @@ class MqttCover(MqttEntity, CoverEntity): position, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: self._state = ( diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 8bddce69cd9..0d1a623eaea 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -42,7 +42,14 @@ from homeassistant.util.percentage import ( from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, +) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -542,6 +549,7 @@ class MqttFan(MqttEntity, FanEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if percentage: await self.async_set_percentage(percentage) @@ -563,6 +571,7 @@ class MqttFan(MqttEntity, FanEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: self._state = False @@ -583,6 +592,7 @@ class MqttFan(MqttEntity, FanEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic_percentage: @@ -606,6 +616,7 @@ class MqttFan(MqttEntity, FanEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic_preset_mode: @@ -632,6 +643,7 @@ class MqttFan(MqttEntity, FanEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic_oscillation: diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 7fcedcbc659..0d4dde8bff2 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -32,7 +32,14 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, +) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -408,6 +415,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: self._state = True @@ -425,6 +433,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: self._state = False @@ -442,6 +451,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic_target_humidity: @@ -465,6 +475,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic_mode: diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index c0fc65610fd..ee83560c8b3 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -53,7 +53,13 @@ import homeassistant.util.color as color_util from .. import subscription from ... import mqtt -from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC +from ..const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, +) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA @@ -821,6 +827,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) def scale_rgbx(color, brightness=None): @@ -1069,6 +1076,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._payload["off"], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 0ba796df4e7..2eb74035bbb 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -59,7 +59,13 @@ import homeassistant.util.color as color_util from .. import subscription from ... import mqtt -from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC +from ..const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, +) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA @@ -629,6 +635,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): json.dumps(message), self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: @@ -654,6 +661,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): json.dumps(message), self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 838daab5860..ff9058384e0 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -35,7 +35,13 @@ import homeassistant.util.color as color_util from .. import subscription from ... import mqtt -from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC +from ..const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, +) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA @@ -384,6 +390,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): ), self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: @@ -409,6 +416,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): ), self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index bd9fc21563f..baed3a28e1e 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -17,7 +17,14 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS, subscription from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, +) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -175,6 +182,7 @@ class MqttLock(MqttEntity, LockEntity): self._config[CONF_PAYLOAD_LOCK], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: # Optimistically assume that the lock has changed state. @@ -192,6 +200,7 @@ class MqttLock(MqttEntity, LockEntity): self._config[CONF_PAYLOAD_UNLOCK], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: # Optimistically assume that the lock has changed state. @@ -209,6 +218,7 @@ class MqttLock(MqttEntity, LockEntity): self._config[CONF_PAYLOAD_OPEN], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: # Optimistically assume that the lock unlocks when opened. diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 3362c8dc3ce..fd849e3731b 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -29,7 +29,14 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, +) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -258,6 +265,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) @property diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index c4fabfc545b..b12eb2d336a 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -17,7 +17,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN +from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN, DOMAIN from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, @@ -153,4 +153,5 @@ class MqttScene( self._config[CONF_PAYLOAD_ON], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index c6b404616ca..195f1753d8f 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -19,7 +19,14 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS, MqttCommandTemplate, subscription from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, +) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -184,6 +191,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): payload, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) @property diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f21e89c32c1..0fdcebe6ff2 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -26,7 +26,14 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import PLATFORMS, subscription from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN +from .const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + DOMAIN, +) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -188,6 +195,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._config[CONF_PAYLOAD_ON], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: # Optimistically assume that switch has changed state. @@ -205,6 +213,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._config[CONF_PAYLOAD_OFF], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) if self._optimistic: # Optimistically assume that switch has changed state. diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index b19d4afe23e..ca538fe3cb6 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -26,7 +26,7 @@ from homeassistant.helpers.icon import icon_for_battery_level from .. import subscription from ... import mqtt -from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN +from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED @@ -199,6 +199,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._fan_speed_list = config[CONF_FAN_SPEED_LIST] self._qos = config[CONF_QOS] self._retain = config[CONF_RETAIN] + self._encoding = config[CONF_ENCODING] self._command_topic = config.get(CONF_COMMAND_TOPIC) self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) @@ -388,6 +389,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._payloads[CONF_PAYLOAD_TURN_ON], self._qos, self._retain, + self._encoding, ) self._status = "Cleaning" self.async_write_ha_state() @@ -403,6 +405,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._payloads[CONF_PAYLOAD_TURN_OFF], self._qos, self._retain, + self._encoding, ) self._status = "Turning Off" self.async_write_ha_state() @@ -418,6 +421,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._payloads[CONF_PAYLOAD_STOP], self._qos, self._retain, + self._encoding, ) self._status = "Stopping the current task" self.async_write_ha_state() @@ -433,6 +437,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._payloads[CONF_PAYLOAD_CLEAN_SPOT], self._qos, self._retain, + self._encoding, ) self._status = "Cleaning spot" self.async_write_ha_state() @@ -448,6 +453,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._payloads[CONF_PAYLOAD_LOCATE], self._qos, self._retain, + self._encoding, ) self._status = "Hi, I'm over here!" self.async_write_ha_state() @@ -463,6 +469,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._payloads[CONF_PAYLOAD_START_PAUSE], self._qos, self._retain, + self._encoding, ) self._status = "Pausing/Resuming cleaning..." self.async_write_ha_state() @@ -478,6 +485,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._payloads[CONF_PAYLOAD_RETURN_TO_BASE], self._qos, self._retain, + self._encoding, ) self._status = "Returning home..." self.async_write_ha_state() @@ -490,7 +498,12 @@ class MqttVacuum(MqttEntity, VacuumEntity): return None await mqtt.async_publish( - self.hass, self._set_fan_speed_topic, fan_speed, self._qos, self._retain + self.hass, + self._set_fan_speed_topic, + fan_speed, + self._qos, + self._retain, + self._encoding, ) self._status = f"Setting fan to {fan_speed}..." self.async_write_ha_state() @@ -506,7 +519,12 @@ class MqttVacuum(MqttEntity, VacuumEntity): else: message = command await mqtt.async_publish( - self.hass, self._send_command_topic, message, self._qos, self._retain + self.hass, + self._send_command_topic, + message, + self._qos, + self._retain, + self._encoding, ) self._status = f"Sending command {message}..." self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 0bfb7289f37..d139ca5eb9b 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -29,7 +29,13 @@ import homeassistant.helpers.config_validation as cv from .. import subscription from ... import mqtt -from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC +from ..const import ( + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, +) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED @@ -248,6 +254,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self._config[CONF_PAYLOAD_START], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def async_pause(self): @@ -260,6 +267,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self._config[CONF_PAYLOAD_PAUSE], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def async_stop(self, **kwargs): @@ -272,6 +280,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self._config[CONF_PAYLOAD_STOP], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def async_set_fan_speed(self, fan_speed, **kwargs): @@ -286,6 +295,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): fan_speed, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def async_return_to_base(self, **kwargs): @@ -298,6 +308,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self._config[CONF_PAYLOAD_RETURN_TO_BASE], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def async_clean_spot(self, **kwargs): @@ -310,6 +321,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self._config[CONF_PAYLOAD_CLEAN_SPOT], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def async_locate(self, **kwargs): @@ -322,6 +334,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self._config[CONF_PAYLOAD_LOCATE], self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) async def async_send_command(self, command, params=None, **kwargs): @@ -340,4 +353,5 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): message, self._config[CONF_QOS], self._config[CONF_RETAIN], + self._config[CONF_ENCODING], ) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 2a74a75c241..a9136bc8d32 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -50,6 +50,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -750,3 +751,58 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template,tpl_par,tpl_output", + [ + ( + alarm_control_panel.SERVICE_ALARM_ARM_AWAY, + "command_topic", + {"code": "secret"}, + "ARM_AWAY", + "command_template", + "code", + b"s", + ), + ( + alarm_control_panel.SERVICE_ALARM_DISARM, + "command_topic", + {"code": "secret"}, + "DISARM", + "command_template", + "code", + b"s", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, + tpl_par, + tpl_output, +): + """Test publishing MQTT payload with different encoding.""" + domain = alarm_control_panel.DOMAIN + config = DEFAULT_CONFIG[domain] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + tpl_par=tpl_par, + tpl_output=tpl_output, + ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 5eb92db7767..115c2daeca2 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -23,6 +23,7 @@ from .test_common import ( help_test_entity_device_info_with_connection, help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -321,3 +322,30 @@ async def test_valid_device_class(hass, mqtt_mock): assert state.attributes["device_class"] == button.ButtonDeviceClass.RESTART state = hass.states.get("button.test_3") assert "device_class" not in state.attributes + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + (button.SERVICE_PRESS, "command_topic", None, "PRESS", None), + ], +) +async def test_publishing_with_custom_encoding( + hass, mqtt_mock, caplog, service, topic, parameters, payload, template +): + """Test publishing MQTT payload with different encoding.""" + domain = button.DOMAIN + config = DEFAULT_CONFIG[domain] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 61f04db99d9..699717ffe09 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -6,6 +6,7 @@ from unittest.mock import call, patch import pytest import voluptuous as vol +from homeassistant.components import climate from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.components.climate.const import ( ATTR_HVAC_ACTION, @@ -46,6 +47,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -1133,3 +1135,121 @@ async def test_precision_whole(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 24.0 mqtt_mock.async_publish.reset_mock() + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + climate.SERVICE_TURN_ON, + "power_command_topic", + None, + "ON", + None, + ), + ( + climate.SERVICE_SET_HVAC_MODE, + "mode_command_topic", + {"hvac_mode": "cool"}, + "cool", + "mode_command_template", + ), + ( + climate.SERVICE_SET_PRESET_MODE, + "away_mode_command_topic", + {"preset_mode": "away"}, + "ON", + None, + ), + ( + climate.SERVICE_SET_PRESET_MODE, + "hold_command_topic", + {"preset_mode": "eco"}, + "eco", + "hold_command_template", + ), + ( + climate.SERVICE_SET_PRESET_MODE, + "hold_command_topic", + {"preset_mode": "some_hold_mode"}, + "some_hold_mode", + "hold_command_template", + ), + ( + climate.SERVICE_SET_FAN_MODE, + "fan_mode_command_topic", + {"fan_mode": "medium"}, + "medium", + "fan_mode_command_template", + ), + ( + climate.SERVICE_SET_SWING_MODE, + "swing_mode_command_topic", + {"swing_mode": "on"}, + "on", + "swing_mode_command_template", + ), + ( + climate.SERVICE_SET_AUX_HEAT, + "aux_command_topic", + {"aux_heat": "on"}, + "ON", + None, + ), + ( + climate.SERVICE_SET_TEMPERATURE, + "temperature_command_topic", + {"temperature": "20.1"}, + 20.1, + "temperature_command_template", + ), + ( + climate.SERVICE_SET_TEMPERATURE, + "temperature_low_command_topic", + { + "temperature": "20.1", + "target_temp_low": "15.1", + "target_temp_high": "29.8", + }, + 15.1, + "temperature_low_command_template", + ), + ( + climate.SERVICE_SET_TEMPERATURE, + "temperature_high_command_topic", + { + "temperature": "20.1", + "target_temp_low": "15.1", + "target_temp_high": "29.8", + }, + 29.8, + "temperature_high_command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = climate.DOMAIN + config = DEFAULT_CONFIG[domain] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 16af5b8e484..93d0587c1dd 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -8,7 +8,7 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.const import MQTT_DISCONNECTED from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component @@ -1266,3 +1266,120 @@ async def help_test_entity_category(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/{unique_id}/config", data) await hass.async_block_till_done() assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, unique_id) + + +async def help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + tpl_par="value", + tpl_output=None, +): + """Test a service with publishing MQTT payload with different encoding.""" + # prepare config for tests + test_config = { + "test1": {"encoding": None, "cmd_tpl": False}, + "test2": {"encoding": "utf-16", "cmd_tpl": False}, + "test3": {"encoding": "", "cmd_tpl": False}, + "test4": {"encoding": "invalid", "cmd_tpl": False}, + "test5": {"encoding": "", "cmd_tpl": True}, + } + setup_config = [] + service_data = {} + for test_id, test_data in test_config.items(): + test_config_setup = copy.deepcopy(config) + test_config_setup.update( + { + topic: f"cmd/{test_id}", + "name": f"{test_id}", + } + ) + if test_data["encoding"] is not None: + test_config_setup["encoding"] = test_data["encoding"] + if test_data["cmd_tpl"]: + test_config_setup[ + template + ] = f"{{{{ (('%.1f'|format({tpl_par}))[0] if is_number({tpl_par}) else {tpl_par}[0]) | ord | pack('b') }}}}" + setup_config.append(test_config_setup) + + # setup service data + service_data[test_id] = {ATTR_ENTITY_ID: f"{domain}.{test_id}"} + if parameters: + service_data[test_id].update(parameters) + + # setup test entities + assert await async_setup_component( + hass, + domain, + {domain: setup_config}, + ) + await hass.async_block_till_done() + + # 1) test with default encoding + await hass.services.async_call( + domain, + service, + service_data["test1"], + blocking=True, + ) + + mqtt_mock.async_publish.assert_any_call("cmd/test1", str(payload), 0, False) + mqtt_mock.async_publish.reset_mock() + + # 2) test with utf-16 encoding + await hass.services.async_call( + domain, + service, + service_data["test2"], + blocking=True, + ) + mqtt_mock.async_publish.assert_any_call( + "cmd/test2", str(payload).encode("utf-16"), 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # 3) test with no encoding set should fail if payload is a string + await hass.services.async_call( + domain, + service, + service_data["test3"], + blocking=True, + ) + assert ( + f"Can't pass-through payload for publishing {payload} on cmd/test3 with no encoding set, need 'bytes'" + in caplog.text + ) + + # 4) test with invalid encoding set should fail + await hass.services.async_call( + domain, + service, + service_data["test4"], + blocking=True, + ) + assert ( + f"Can't encode payload for publishing {payload} on cmd/test4 with encoding invalid" + in caplog.text + ) + + # 5) test with command template and raw encoding if specified + if not template: + return + + await hass.services.async_call( + domain, + service, + service_data["test5"], + blocking=True, + ) + mqtt_mock.async_publish.assert_any_call( + "cmd/test5", tpl_output or str(payload)[0].encode("utf-8"), 0, False + ) + mqtt_mock.async_publish.reset_mock() diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 794d143ac83..6fcda671793 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -61,6 +61,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -3057,3 +3058,58 @@ async def test_tilt_status_template_without_tilt_status_topic_topic( f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." in caplog.text ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + SERVICE_OPEN_COVER, + "command_topic", + None, + "OPEN", + None, + ), + ( + SERVICE_SET_COVER_POSITION, + "set_position_topic", + {ATTR_POSITION: "50"}, + 50, + "set_position_template", + ), + ( + SERVICE_SET_COVER_TILT_POSITION, + "tilt_command_topic", + {ATTR_TILT_POSITION: "45"}, + 45, + "tilt_command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = cover.DOMAIN + config = DEFAULT_CONFIG[domain] + config["position_topic"] = "some-position-topic" + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index c310db335ad..b030a1d4114 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1,4 +1,5 @@ """Test MQTT fans.""" +import copy from unittest.mock import patch import pytest @@ -32,6 +33,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -1663,3 +1665,73 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + fan.SERVICE_TURN_ON, + "command_topic", + None, + "ON", + None, + ), + ( + fan.SERVICE_TURN_OFF, + "command_topic", + None, + "OFF", + None, + ), + ( + fan.SERVICE_SET_PRESET_MODE, + "preset_mode_command_topic", + {fan.ATTR_PRESET_MODE: "eco"}, + "eco", + "preset_mode_command_template", + ), + ( + fan.SERVICE_SET_PERCENTAGE, + "percentage_command_topic", + {fan.ATTR_PERCENTAGE: "45"}, + 45, + "percentage_command_template", + ), + ( + fan.SERVICE_OSCILLATE, + "oscillation_command_topic", + {fan.ATTR_OSCILLATING: "on"}, + "oscillate_on", + "oscillation_command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = fan.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[domain]) + if topic == "preset_mode_command_topic": + config["preset_modes"] = ["auto", "eco"] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 76c0b6e9f8e..5a4ac98c70a 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -1,4 +1,5 @@ """Test MQTT humidifiers.""" +import copy from unittest.mock import patch import pytest @@ -42,6 +43,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -1058,3 +1060,66 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + humidifier.SERVICE_TURN_ON, + "command_topic", + None, + "ON", + None, + ), + ( + humidifier.SERVICE_TURN_OFF, + "command_topic", + None, + "OFF", + None, + ), + ( + humidifier.SERVICE_SET_MODE, + "mode_command_topic", + {humidifier.ATTR_MODE: "eco"}, + "eco", + "mode_command_template", + ), + ( + humidifier.SERVICE_SET_HUMIDITY, + "target_humidity_command_topic", + {humidifier.ATTR_HUMIDITY: "45"}, + 45, + "target_humidity_command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = humidifier.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[domain]) + if topic == "mode_command_topic": + config["modes"] = ["auto", "eco"] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 5d9b50252a4..a9cfb51a213 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -41,6 +41,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -746,3 +747,78 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, vacuum.DOMAIN, config, "test-topic" ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + vacuum.SERVICE_TURN_ON, + "command_topic", + None, + "turn_on", + None, + ), + ( + vacuum.SERVICE_CLEAN_SPOT, + "command_topic", + None, + "clean_spot", + None, + ), + ( + vacuum.SERVICE_SET_FAN_SPEED, + "set_fan_speed_topic", + {"fan_speed": "medium"}, + "medium", + None, + ), + ( + vacuum.SERVICE_SEND_COMMAND, + "send_command_topic", + {"command": "custom command"}, + "custom command", + None, + ), + ( + vacuum.SERVICE_TURN_OFF, + "command_topic", + None, + "turn_off", + None, + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG) + config["supported_features"] = [ + "turn_on", + "turn_off", + "clean_spot", + "fan_speed", + "send_command", + ] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 58b607bb777..53405a4c410 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -153,6 +153,7 @@ light: payload_off: "off" """ +import copy from unittest.mock import call, patch import pytest @@ -189,6 +190,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -3407,3 +3409,125 @@ async def test_reloadable(hass, mqtt_mock): assert hass.states.get("light.test") is None assert hass.states.get("light.reload") + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template,tpl_par,tpl_output", + [ + ( + light.SERVICE_TURN_ON, + "command_topic", + None, + "ON", + None, + None, + None, + ), + ( + light.SERVICE_TURN_ON, + "white_command_topic", + {"white": "255"}, + 255, + None, + None, + None, + ), + ( + light.SERVICE_TURN_ON, + "brightness_command_topic", + {"color_temp": "200", "brightness": "50"}, + 50, + None, + None, + None, + ), + ( + light.SERVICE_TURN_ON, + "effect_command_topic", + {"rgb_color": [255, 128, 0], "effect": "color_loop"}, + "color_loop", + None, + None, + None, + ), + ( + light.SERVICE_TURN_ON, + "color_temp_command_topic", + {"color_temp": "200"}, + 200, + "color_temp_command_template", + "value", + b"2", + ), + ( + light.SERVICE_TURN_ON, + "rgb_command_topic", + {"rgb_color": [255, 128, 0]}, + "255,128,0", + "rgb_command_template", + "red", + b"2", + ), + ( + light.SERVICE_TURN_ON, + "hs_command_topic", + {"rgb_color": [255, 128, 0]}, + "30.118,100.0", + None, + None, + None, + ), + ( + light.SERVICE_TURN_ON, + "xy_command_topic", + {"hs_color": [30.118, 100.0]}, + "0.611,0.375", + None, + None, + None, + ), + ( + light.SERVICE_TURN_OFF, + "command_topic", + None, + "OFF", + None, + None, + None, + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, + tpl_par, + tpl_output, +): + """Test publishing MQTT payload with different encoding.""" + domain = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[domain]) + if topic == "effect_command_topic": + config["effect_list"] = ["random", "color_loop"] + elif topic == "white_command_topic": + config["rgb_command_topic"] = "some-cmd-topic" + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + tpl_par=tpl_par, + tpl_output=tpl_output, + ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 677509277c7..ca25d737d0e 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -87,6 +87,7 @@ light: brightness: true brightness_scale: 99 """ +import copy import json from unittest.mock import call, patch @@ -122,6 +123,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -1902,3 +1904,62 @@ async def test_max_mireds(hass, mqtt_mock): state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 assert state.attributes.get("max_mireds") == 370 + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template,tpl_par,tpl_output", + [ + ( + light.SERVICE_TURN_ON, + "command_topic", + None, + '{"state": "ON"}', + None, + None, + None, + ), + ( + light.SERVICE_TURN_OFF, + "command_topic", + None, + '{"state": "OFF"}', + None, + None, + None, + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, + tpl_par, + tpl_output, +): + """Test publishing MQTT payload with different encoding.""" + domain = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[domain]) + if topic == "effect_command_topic": + config["effect_list"] = ["random", "color_loop"] + elif topic == "white_command_topic": + config["rgb_command_topic"] = "some-cmd-topic" + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + tpl_par=tpl_par, + tpl_output=tpl_output, + ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index fe2d9badf7d..6bb5953963c 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -26,6 +26,7 @@ If your light doesn't support white value feature, omit `white_value_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ +import copy from unittest.mock import patch import pytest @@ -60,6 +61,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -1085,3 +1087,62 @@ async def test_max_mireds(hass, mqtt_mock): state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 assert state.attributes.get("max_mireds") == 370 + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template,tpl_par,tpl_output", + [ + ( + light.SERVICE_TURN_ON, + "command_topic", + None, + "on,", + None, + None, + None, + ), + ( + light.SERVICE_TURN_OFF, + "command_topic", + None, + "off,", + None, + None, + None, + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, + tpl_par, + tpl_output, +): + """Test publishing MQTT payload with different encoding.""" + domain = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[domain]) + if topic == "effect_command_topic": + config["effect_list"] = ["random", "color_loop"] + elif topic == "white_command_topic": + config["rgb_command_topic"] = "some-cmd-topic" + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + tpl_par=tpl_par, + tpl_output=tpl_output, + ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 8d76e46f32b..0aa1172c2ec 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -37,6 +37,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -589,3 +590,43 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + SERVICE_LOCK, + "command_topic", + None, + "LOCK", + None, + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = LOCK_DOMAIN + config = DEFAULT_CONFIG[domain] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 797a7b894fc..463bfc8a2c2 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -43,6 +43,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -640,3 +641,43 @@ async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): assert ( "Invalid value for number.test_number: 115.5 (range 5.0 - 110.0)" in caplog.text ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + SERVICE_SET_VALUE, + "command_topic", + {ATTR_VALUE: "45"}, + 45, + "command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = NUMBER_DOMAIN + config = DEFAULT_CONFIG[domain] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 2f31ce788fd..2bc7b67574f 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -33,6 +33,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -524,3 +525,37 @@ async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): "Invalid option for select.test_select: 'öl' (valid options: ['milk', 'beer'])" in caplog.text ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + select.SERVICE_SELECT_OPTION, + "command_topic", + {"option": "beer"}, + "beer", + "command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, mqtt_mock, caplog, service, topic, parameters, payload, template +): + """Test publishing MQTT payload with different encoding.""" + domain = select.DOMAIN + config = DEFAULT_CONFIG[domain] + config["options"] = ["milk", "beer"] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index f53f8ebdab3..ae2f68f8e70 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -51,6 +51,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -502,3 +503,83 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2, payload="{}" ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + vacuum.SERVICE_START, + "command_topic", + None, + "start", + None, + ), + ( + vacuum.SERVICE_CLEAN_SPOT, + "command_topic", + None, + "clean_spot", + None, + ), + ( + vacuum.SERVICE_SET_FAN_SPEED, + "set_fan_speed_topic", + {"fan_speed": "medium"}, + "medium", + None, + ), + ( + vacuum.SERVICE_SEND_COMMAND, + "send_command_topic", + {"command": "custom command"}, + "custom command", + None, + ), + ( + vacuum.SERVICE_STOP, + "command_topic", + None, + "stop", + None, + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG) + config["supported_features"] = [ + "battery", + "clean_spot", + "fan_speed", + "locate", + "pause", + "return_home", + "send_command", + "start", + "status", + "stop", + ] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index a3ef29d0d08..1932f799654 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -32,6 +32,7 @@ from .test_common import ( help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, @@ -467,3 +468,50 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + switch.SERVICE_TURN_ON, + "command_topic", + None, + "ON", + None, + ), + ( + switch.SERVICE_TURN_OFF, + "command_topic", + None, + "OFF", + None, + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with different encoding.""" + domain = switch.DOMAIN + config = DEFAULT_CONFIG[domain] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + )