]> git.basschouten.com Git - openhab-addons.git/blob
6eb054499410eb99e8a6f3bf4042dad922662dcc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.amazonechocontrol.internal.handler;
14
15 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_ID;
16 import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.SUPPORTED_INTERFACES;
17
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.Set;
28 import java.util.function.Function;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.amazonechocontrol.internal.Connection;
33 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapabilities.SmartHomeCapability;
34 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevices.SmartHomeDevice;
35 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentifiers;
36 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroupIdentity;
37 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeGroups.SmartHomeGroup;
38 import org.openhab.binding.amazonechocontrol.internal.jsons.JsonSmartHomeTags;
39 import org.openhab.binding.amazonechocontrol.internal.jsons.SmartHomeBaseDevice;
40 import org.openhab.binding.amazonechocontrol.internal.smarthome.Constants;
41 import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase;
42 import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase.ChannelInfo;
43 import org.openhab.binding.amazonechocontrol.internal.smarthome.HandlerBase.UpdateChannelResult;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.Channel;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseThingHandler;
51 import org.openhab.core.thing.binding.BridgeHandler;
52 import org.openhab.core.thing.binding.builder.ChannelBuilder;
53 import org.openhab.core.thing.binding.builder.ThingBuilder;
54 import org.openhab.core.thing.type.ChannelTypeUID;
55 import org.openhab.core.types.Command;
56 import org.openhab.core.types.RefreshType;
57 import org.openhab.core.types.State;
58 import org.openhab.core.types.StateDescription;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import com.google.gson.Gson;
63 import com.google.gson.JsonArray;
64 import com.google.gson.JsonElement;
65 import com.google.gson.JsonNull;
66 import com.google.gson.JsonObject;
67
68 /**
69  * @author Lukas Knoeller - Initial contribution
70  */
71 @NonNullByDefault
72 public class SmartHomeDeviceHandler extends BaseThingHandler {
73     private final Logger logger = LoggerFactory.getLogger(SmartHomeDeviceHandler.class);
74
75     private @Nullable SmartHomeBaseDevice smartHomeBaseDevice;
76     private final Gson gson;
77     private final Map<String, HandlerBase> handlers = new HashMap<>();
78     private final Map<String, JsonArray> lastStates = new HashMap<>();
79
80     public SmartHomeDeviceHandler(Thing thing, Gson gson) {
81         super(thing);
82         this.gson = gson;
83     }
84
85     public synchronized void setDeviceAndUpdateThingState(AccountHandler accountHandler,
86             @Nullable SmartHomeBaseDevice smartHomeBaseDevice) {
87         if (smartHomeBaseDevice == null) {
88             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Can't find smarthomeBaseDevice");
89             return;
90         }
91         boolean changed = this.smartHomeBaseDevice == null;
92         this.smartHomeBaseDevice = smartHomeBaseDevice;
93
94         Set<String> unusedChannels = new HashSet<>();
95         thing.getChannels().forEach(channel -> unusedChannels.add(channel.getUID().getId()));
96
97         Set<String> unusedHandlers = new HashSet<>(handlers.keySet());
98
99         Map<String, List<SmartHomeCapability>> capabilities = new HashMap<>();
100         getCapabilities(capabilities, accountHandler, smartHomeBaseDevice);
101
102         ThingBuilder thingBuilder = editThing();
103
104         for (String interfaceName : capabilities.keySet()) {
105             HandlerBase handler = handlers.get(interfaceName);
106             if (handler != null) {
107                 unusedHandlers.remove(interfaceName);
108             } else {
109                 Function<SmartHomeDeviceHandler, HandlerBase> creator = Constants.HANDLER_FACTORY.get(interfaceName);
110                 if (creator != null) {
111                     handler = creator.apply(this);
112                     handlers.put(interfaceName, handler);
113                 }
114             }
115             if (handler != null) {
116                 Collection<ChannelInfo> required = handler
117                         .initialize(capabilities.getOrDefault(interfaceName, List.of()));
118                 for (ChannelInfo channelInfo : required) {
119                     unusedChannels.remove(channelInfo.channelId);
120                     if (addChannelToDevice(thingBuilder, channelInfo.channelId, channelInfo.itemType,
121                             channelInfo.channelTypeUID)) {
122                         changed = true;
123                     }
124                 }
125             }
126         }
127
128         unusedHandlers.forEach(handlers::remove);
129         if (!unusedChannels.isEmpty()) {
130             changed = true;
131             unusedChannels.stream().map(id -> new ChannelUID(thing.getUID(), id)).forEach(thingBuilder::withoutChannel);
132         }
133
134         if (changed) {
135             updateThing(thingBuilder.build());
136             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Thing has changed.");
137             accountHandler.forceDelayedSmartHomeStateUpdate(getId());
138         }
139     }
140
141     public String getId() {
142         String id = (String) getConfig().get(DEVICE_PROPERTY_ID);
143         if (id == null) {
144             return "";
145         }
146         return id;
147     }
148
149     @Override
150     public void updateState(String channelId, State state) {
151         super.updateState(new ChannelUID(thing.getUID(), channelId), state);
152     }
153
154     @Override
155     public void initialize() {
156         AccountHandler accountHandler = getAccountHandler();
157         if (accountHandler != null) {
158             accountHandler.addSmartHomeDeviceHandler(this);
159             setDeviceAndUpdateThingState(accountHandler, smartHomeBaseDevice);
160         } else {
161             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridgehandler not found");
162         }
163     }
164
165     private boolean addChannelToDevice(ThingBuilder thingBuilder, String channelId, String itemType,
166             ChannelTypeUID channelTypeUID) {
167         Channel channel = thing.getChannel(channelId);
168         if (channel != null) {
169             if (channelTypeUID.equals(channel.getChannelTypeUID()) && itemType.equals(channel.getAcceptedItemType())) {
170                 // channel exist with the same settings
171                 return false;
172             }
173             // channel exist with other settings, remove it first
174             thingBuilder.withoutChannel(channel.getUID());
175         }
176         thingBuilder.withChannel(ChannelBuilder.create(new ChannelUID(thing.getUID(), channelId), itemType)
177                 .withType(channelTypeUID).build());
178         return true;
179     }
180
181     public void updateChannelStates(List<SmartHomeBaseDevice> allDevices,
182             Map<String, JsonArray> applianceIdToCapabilityStates) {
183         logger.trace("Updating {} with {}", allDevices, applianceIdToCapabilityStates);
184         AccountHandler accountHandler = getAccountHandler();
185         SmartHomeBaseDevice smartHomeBaseDevice = this.smartHomeBaseDevice;
186         if (smartHomeBaseDevice == null) {
187             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Can't find smarthomeBaseDevice!");
188             return;
189         }
190
191         boolean stateFound = false;
192         Map<String, List<JsonObject>> mapInterfaceToStates = new HashMap<>();
193         SmartHomeDevice firstDevice = null;
194         for (SmartHomeDevice shd : getSupportedSmartHomeDevices(smartHomeBaseDevice, allDevices)) {
195             String applianceId = shd.applianceId;
196             if (applianceId == null) {
197                 continue;
198             }
199             JsonArray states = applianceIdToCapabilityStates.get(applianceId);
200             if (states != null) {
201                 stateFound = true;
202                 if (smartHomeBaseDevice.isGroup()) {
203                     // for groups, store the last state of all devices
204                     lastStates.put(applianceId, states);
205                 }
206             } else {
207                 states = lastStates.get(applianceId);
208                 if (states == null) {
209                     continue;
210                 }
211             }
212             if (firstDevice == null) {
213                 firstDevice = shd;
214             }
215             for (JsonElement stateElement : states) {
216                 String stateJson = stateElement.getAsString();
217                 if (stateJson.startsWith("{") && stateJson.endsWith("}")) {
218                     JsonObject state = Objects.requireNonNull(gson.fromJson(stateJson, JsonObject.class));
219                     String interfaceName = Objects.requireNonNullElse(state.get("namespace"), JsonNull.INSTANCE)
220                             .getAsString();
221                     Objects.requireNonNull(mapInterfaceToStates.computeIfAbsent(interfaceName, k -> new ArrayList<>()))
222                             .add(state);
223                 }
224             }
225         }
226
227         for (HandlerBase handlerBase : handlers.values()) {
228             UpdateChannelResult result = new UpdateChannelResult();
229             for (String interfaceName : handlerBase.getSupportedInterface()) {
230                 List<JsonObject> stateList = mapInterfaceToStates.get(interfaceName);
231                 if (stateList != null) {
232                     try {
233                         handlerBase.updateChannels(interfaceName, stateList, result);
234                     } catch (Exception e) {
235                         // We catch all exceptions, otherwise all other things are not updated!
236                         logger.debug("Updating states failed", e);
237                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
238                                 e.getLocalizedMessage());
239                     }
240                 }
241             }
242
243             if (result.needSingleUpdate && smartHomeBaseDevice instanceof SmartHomeDevice && accountHandler != null) {
244                 SmartHomeDevice shd = (SmartHomeDevice) smartHomeBaseDevice;
245                 accountHandler.forceDelayedSmartHomeStateUpdate(shd.findId());
246             }
247         }
248
249         if (stateFound) {
250             updateStatus(ThingStatus.ONLINE);
251         } else {
252             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "State not found");
253         }
254     }
255
256     private @Nullable AccountHandler getAccountHandler() {
257         Bridge bridge = getBridge();
258         if (bridge != null) {
259             BridgeHandler bridgeHandler = bridge.getHandler();
260             if (bridgeHandler instanceof AccountHandler) {
261                 return (AccountHandler) bridgeHandler;
262             }
263         }
264
265         return null;
266     }
267
268     @Override
269     public void handleCommand(ChannelUID channelUID, Command command) {
270         AccountHandler accountHandler = getAccountHandler();
271         if (accountHandler == null) {
272             logger.debug("accountHandler is null in {}", thing.getUID());
273             return;
274         }
275         Connection connection = accountHandler.findConnection();
276         if (connection == null) {
277             logger.debug("connection is null in {}", thing.getUID());
278             return;
279         }
280
281         try {
282             if (command instanceof RefreshType) {
283                 accountHandler.forceDelayedSmartHomeStateUpdate(getId());
284                 return;
285             }
286             SmartHomeBaseDevice smartHomeBaseDevice = this.smartHomeBaseDevice;
287             if (smartHomeBaseDevice == null) {
288                 logger.debug("smarthomeBaseDevice is null in {}", thing.getUID());
289                 return;
290             }
291             Set<SmartHomeDevice> devices = getSupportedSmartHomeDevices(smartHomeBaseDevice,
292                     accountHandler.getLastKnownSmartHomeDevices());
293             String channelId = channelUID.getId();
294
295             for (String interfaceName : handlers.keySet()) {
296                 HandlerBase handlerBase = handlers.get(interfaceName);
297                 if (handlerBase == null || !handlerBase.hasChannel(channelId)) {
298                     continue;
299                 }
300                 for (SmartHomeDevice shd : devices) {
301                     String entityId = shd.entityId;
302                     if (entityId == null) {
303                         continue;
304                     }
305                     accountHandler.forceDelayedSmartHomeStateUpdate(getId()); // block updates
306                     if (handlerBase.handleCommand(connection, shd, entityId, shd.getCapabilities(), channelUID.getId(),
307                             command)) {
308                         accountHandler.forceDelayedSmartHomeStateUpdate(getId()); // force update again to restart
309                         // update timer
310                         logger.debug("Command {} sent to {}", command, shd.findId());
311                     }
312                 }
313             }
314         } catch (Exception e) {
315             logger.warn("Handle command failed", e);
316         }
317     }
318
319     private static void getCapabilities(Map<String, List<SmartHomeCapability>> result, AccountHandler accountHandler,
320             SmartHomeBaseDevice device) {
321         if (device instanceof SmartHomeDevice) {
322             SmartHomeDevice shd = (SmartHomeDevice) device;
323             for (SmartHomeCapability capability : shd.getCapabilities()) {
324                 String interfaceName = capability.interfaceName;
325                 if (interfaceName != null) {
326                     Objects.requireNonNull(result.computeIfAbsent(interfaceName, name -> new ArrayList<>()))
327                             .add(capability);
328                 }
329             }
330         }
331         if (device instanceof SmartHomeGroup) {
332             for (SmartHomeDevice shd : getSupportedSmartHomeDevices(device,
333                     accountHandler.getLastKnownSmartHomeDevices())) {
334                 getCapabilities(result, accountHandler, shd);
335             }
336         }
337     }
338
339     public static Set<SmartHomeDevice> getSupportedSmartHomeDevices(@Nullable SmartHomeBaseDevice baseDevice,
340             List<SmartHomeBaseDevice> allDevices) {
341         if (baseDevice == null) {
342             return Collections.emptySet();
343         }
344         Set<SmartHomeDevice> result = new HashSet<>();
345         if (baseDevice instanceof SmartHomeDevice) {
346             SmartHomeDevice shd = (SmartHomeDevice) baseDevice;
347             if (shd.getCapabilities().stream().map(capability -> capability.interfaceName)
348                     .anyMatch(SUPPORTED_INTERFACES::contains)) {
349                 result.add(shd);
350             }
351         } else {
352             SmartHomeGroup shg = (SmartHomeGroup) baseDevice;
353             for (SmartHomeBaseDevice device : allDevices) {
354                 if (device instanceof SmartHomeDevice) {
355                     SmartHomeDevice shd = (SmartHomeDevice) device;
356                     JsonSmartHomeTags.JsonSmartHomeTag tags = shd.tags;
357                     if (tags != null) {
358                         JsonSmartHomeGroupIdentity.SmartHomeGroupIdentity tagNameToValueSetMap = tags.tagNameToValueSetMap;
359                         JsonSmartHomeGroupIdentifiers.SmartHomeGroupIdentifier applianceGroupIdentifier = shg.applianceGroupIdentifier;
360                         if (tagNameToValueSetMap != null) {
361                             List<String> groupIdentity = Objects.requireNonNullElse(tagNameToValueSetMap.groupIdentity,
362                                     List.of());
363                             if (applianceGroupIdentifier != null && applianceGroupIdentifier.value != null
364                                     && groupIdentity.contains(applianceGroupIdentifier.value)) {
365                                 if (shd.getCapabilities().stream().map(capability -> capability.interfaceName)
366                                         .anyMatch(SUPPORTED_INTERFACES::contains)) {
367                                     result.add(shd);
368                                 }
369                             }
370                         }
371                     }
372                 }
373             }
374         }
375         return result;
376     }
377
378     public @Nullable StateDescription findStateDescription(Channel channel, StateDescription originalStateDescription,
379             @Nullable Locale locale) {
380         String channelId = channel.getUID().getId();
381         for (HandlerBase handler : handlers.values()) {
382             if (handler.hasChannel(channelId)) {
383                 return handler.findStateDescription(channelId, originalStateDescription, locale);
384             }
385         }
386         return null;
387     }
388 }