]> git.basschouten.com Git - openhab-addons.git/blob
fd4512ff12495877ffaced31b5da4649791018db
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
14
15 import java.math.BigDecimal;
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.Objects;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
23 import org.openhab.binding.mqtt.generic.values.OnOffValue;
24 import org.openhab.binding.mqtt.generic.values.PercentageValue;
25 import org.openhab.binding.mqtt.generic.values.TextValue;
26 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
27 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
28 import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelTransformation;
29 import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
30 import org.openhab.core.library.types.DecimalType;
31 import org.openhab.core.library.types.HSBType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.PercentType;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.library.unit.Units;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.type.AutoUpdatePolicy;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.State;
41 import org.openhab.core.types.UnDefType;
42 import org.openhab.core.util.ColorUtil;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 /**
47  * A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
48  *
49  * Specifically, the template schema. All channels are synthetic, and wrap the single internal raw
50  * state.
51  *
52  * @author Cody Cutrer - Initial contribution
53  */
54 @NonNullByDefault
55 public class TemplateSchemaLight extends AbstractRawSchemaLight {
56     private final Logger logger = LoggerFactory.getLogger(TemplateSchemaLight.class);
57     private final HomeAssistantChannelTransformation transformation;
58
59     private static class TemplateVariables {
60         public static final String STATE = "state";
61         public static final String TRANSITION = "transition";
62         public static final String BRIGHTNESS = "brightness";
63         public static final String COLOR_TEMP = "color_temp";
64         public static final String RED = "red";
65         public static final String GREEN = "green";
66         public static final String BLUE = "blue";
67         public static final String HUE = "hue";
68         public static final String SAT = "sat";
69         public static final String FLASH = "flash";
70         public static final String EFFECT = "effect";
71     }
72
73     public TemplateSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
74         super(builder, newStyleChannels);
75         transformation = new HomeAssistantChannelTransformation(getJinjava(), this, "");
76     }
77
78     @Override
79     protected void buildChannels() {
80         AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
81         if (channelConfiguration.commandOnTemplate == null || channelConfiguration.commandOffTemplate == null) {
82             throw new UnsupportedComponentException("Template schema light component '" + getHaID()
83                     + "' does not define command_on_template or command_off_template!");
84         }
85
86         onOffValue = new OnOffValue("on", "off");
87         brightnessValue = new PercentageValue(null, new BigDecimal(255), null, null, null);
88
89         if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null
90                 && channelConfiguration.blueTemplate != null) {
91             colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
92                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command))
93                     .withAutoUpdatePolicy(autoUpdatePolicy).build();
94         } else if (channelConfiguration.brightnessTemplate != null) {
95             brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
96                     "Brightness", this).commandTopic(DUMMY_TOPIC, true, 1)
97                     .commandFilter(command -> handleCommand(command)).withAutoUpdatePolicy(autoUpdatePolicy).build();
98         } else {
99             onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
100                     this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command))
101                     .withAutoUpdatePolicy(autoUpdatePolicy).build();
102         }
103
104         if (channelConfiguration.colorTempTemplate != null) {
105             buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
106                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
107                     .withAutoUpdatePolicy(autoUpdatePolicy).build();
108         }
109         TextValue localEffectValue = effectValue;
110         if (channelConfiguration.effectTemplate != null && localEffectValue != null) {
111             buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, localEffectValue, "Effect", this)
112                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command))
113                     .withAutoUpdatePolicy(autoUpdatePolicy).build();
114         }
115     }
116
117     private static BigDecimal factor = new BigDecimal("2.55"); // string to not lose precision
118
119     @Override
120     protected void publishState(HSBType state) {
121         Map<String, @Nullable Object> binding = new HashMap<>();
122         String template;
123
124         logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
125         if (state.getBrightness().equals(PercentType.ZERO)) {
126             template = Objects.requireNonNull(channelConfiguration.commandOffTemplate);
127             binding.put(TemplateVariables.STATE, "off");
128         } else {
129             template = Objects.requireNonNull(channelConfiguration.commandOnTemplate);
130             binding.put(TemplateVariables.STATE, "on");
131             if (channelConfiguration.brightnessTemplate != null) {
132                 binding.put(TemplateVariables.BRIGHTNESS,
133                         state.getBrightness().toBigDecimal().multiply(factor).intValue());
134             }
135             if (colorChannel != null) {
136                 int[] rgb = ColorUtil.hsbToRgb(state);
137                 binding.put(TemplateVariables.RED, rgb[0]);
138                 binding.put(TemplateVariables.GREEN, rgb[1]);
139                 binding.put(TemplateVariables.BLUE, rgb[2]);
140                 binding.put(TemplateVariables.HUE, state.getHue().toBigDecimal());
141                 binding.put(TemplateVariables.SAT, state.getSaturation().toBigDecimal());
142             }
143         }
144
145         publishState(binding, template);
146     }
147
148     private boolean handleColorTempCommand(Command command) {
149         if (command instanceof DecimalType) {
150             command = new QuantityType<>(((DecimalType) command).toBigDecimal(), Units.MIRED);
151         }
152         if (command instanceof QuantityType quantity) {
153             QuantityType<?> mireds = quantity.toInvertibleUnit(Units.MIRED);
154             if (mireds == null) {
155                 logger.warn("Unable to convert {} to mireds", command);
156                 return false;
157             }
158
159             Map<String, @Nullable Object> binding = new HashMap<>();
160
161             binding.put(TemplateVariables.STATE, "on");
162             binding.put(TemplateVariables.COLOR_TEMP, mireds.toBigDecimal().intValue());
163
164             publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
165         }
166         return false;
167     }
168
169     private boolean handleEffectCommand(Command command) {
170         if (!(command instanceof StringType)) {
171             return false;
172         }
173
174         Map<String, @Nullable Object> binding = new HashMap<>();
175
176         binding.put(TemplateVariables.STATE, "on");
177         binding.put(TemplateVariables.EFFECT, command.toString());
178
179         publishState(binding, Objects.requireNonNull(channelConfiguration.commandOnTemplate));
180         return false;
181     }
182
183     private void publishState(Map<String, @Nullable Object> binding, String template) {
184         String command;
185
186         command = transform(template, binding);
187         if (command == null) {
188             return;
189         }
190
191         logger.debug("Publishing new state '{}' of light {} to MQTT.", command, getHaID().toShortTopic());
192         rawChannel.getState().publishValue(new StringType(command));
193     }
194
195     @Override
196     public void updateChannelState(ChannelUID channel, State state) {
197         ChannelStateUpdateListener listener = this.channelStateUpdateListener;
198
199         String value;
200
201         String template = channelConfiguration.stateTemplate;
202         if (template != null) {
203             value = transform(template, state.toString());
204             if (value == null || value.isEmpty()) {
205                 onOffValue.update(UnDefType.NULL);
206             } else if ("on".equals(value)) {
207                 onOffValue.update(OnOffType.ON);
208             } else if ("off".equals(value)) {
209                 onOffValue.update(OnOffType.OFF);
210             } else {
211                 logger.warn("Invalid state value '{}' for component {}; expected 'on' or 'off'.", value,
212                         getHaID().toShortTopic());
213                 onOffValue.update(UnDefType.UNDEF);
214             }
215             if (brightnessValue.getChannelState() instanceof UnDefType
216                     && !(onOffValue.getChannelState() instanceof UnDefType)) {
217                 brightnessValue.update(
218                         (PercentType) Objects.requireNonNull(onOffValue.getChannelState().as(PercentType.class)));
219             }
220             if (colorValue.getChannelState() instanceof UnDefType) {
221                 colorValue.update((OnOffType) onOffValue.getChannelState());
222             }
223         }
224
225         template = channelConfiguration.brightnessTemplate;
226         if (template != null) {
227             Integer brightness = getColorChannelValue(template, state.toString());
228             if (brightness == null) {
229                 brightnessValue.update(UnDefType.NULL);
230                 colorValue.update(UnDefType.NULL);
231             } else {
232                 brightnessValue.update((PercentType) brightnessValue.parseMessage(new DecimalType(brightness)));
233                 if (colorValue.getChannelState() instanceof HSBType color) {
234                     colorValue.update(new HSBType(color.getHue(), color.getSaturation(),
235                             (PercentType) brightnessValue.getChannelState()));
236                 } else {
237                     colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
238                             (PercentType) brightnessValue.getChannelState()));
239                 }
240             }
241         }
242
243         @Nullable
244         String redTemplate, greenTemplate, blueTemplate;
245         if ((redTemplate = channelConfiguration.redTemplate) != null
246                 && (greenTemplate = channelConfiguration.greenTemplate) != null
247                 && (blueTemplate = channelConfiguration.blueTemplate) != null) {
248             Integer red = getColorChannelValue(redTemplate, state.toString());
249             Integer green = getColorChannelValue(greenTemplate, state.toString());
250             Integer blue = getColorChannelValue(blueTemplate, state.toString());
251             if (red == null || green == null || blue == null) {
252                 colorValue.update(UnDefType.NULL);
253             } else {
254                 colorValue.update(HSBType.fromRGB(red, green, blue));
255             }
256         }
257         ComponentChannel localBrightnessChannel = brightnessChannel;
258         ComponentChannel localColorChannel = colorChannel;
259         if (localColorChannel != null) {
260             listener.updateChannelState(localColorChannel.getChannel().getUID(), colorValue.getChannelState());
261         } else if (localBrightnessChannel != null) {
262             listener.updateChannelState(localBrightnessChannel.getChannel().getUID(),
263                     brightnessValue.getChannelState());
264         } else {
265             listener.updateChannelState(onOffChannel.getChannel().getUID(), onOffValue.getChannelState());
266         }
267
268         template = channelConfiguration.effectTemplate;
269         if (template != null) {
270             value = transform(template, state.toString());
271             if (value == null || value.isEmpty()) {
272                 effectValue.update(UnDefType.NULL);
273             } else {
274                 effectValue.update(new StringType(value));
275             }
276             listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), effectValue.getChannelState());
277         }
278
279         template = channelConfiguration.colorTempTemplate;
280         if (template != null) {
281             Integer mireds = getColorChannelValue(template, state.toString());
282             if (mireds == null) {
283                 colorTempValue.update(UnDefType.NULL);
284             } else {
285                 colorTempValue.update(new QuantityType(mireds, Units.MIRED));
286             }
287             listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), colorTempValue.getChannelState());
288         }
289     }
290
291     private @Nullable Integer getColorChannelValue(String template, String value) {
292         Object result = transform(template, value);
293         if (result == null) {
294             return null;
295         }
296
297         String string = result.toString();
298         if (string.isEmpty()) {
299             return null;
300         }
301         try {
302             return Integer.parseInt(result.toString());
303         } catch (NumberFormatException e) {
304             logger.warn("Applying template {} for component {} failed: {}", template, getHaID().toShortTopic(),
305                     e.getMessage());
306             return null;
307         }
308     }
309
310     private @Nullable String transform(String template, Map<String, @Nullable Object> binding) {
311         return transformation.apply(template, binding).orElse(null);
312     }
313
314     private @Nullable String transform(String template, String value) {
315         return transformation.apply(template, value).orElse(null);
316     }
317 }