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