2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.amazonechocontrol.internal.handler;
15 import static org.openhab.binding.amazonechocontrol.internal.AmazonEchoControlBindingConstants.DEVICE_PROPERTY_ID;
16 import static org.openhab.binding.amazonechocontrol.internal.smarthome.Constants.SUPPORTED_INTERFACES;
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;
26 import java.util.Objects;
28 import java.util.function.Function;
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;
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;
69 * @author Lukas Knoeller - Initial contribution
72 public class SmartHomeDeviceHandler extends BaseThingHandler {
73 private final Logger logger = LoggerFactory.getLogger(SmartHomeDeviceHandler.class);
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<>();
80 public SmartHomeDeviceHandler(Thing thing, Gson gson) {
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");
91 boolean changed = this.smartHomeBaseDevice == null;
92 this.smartHomeBaseDevice = smartHomeBaseDevice;
94 Set<String> unusedChannels = new HashSet<>();
95 thing.getChannels().forEach(channel -> unusedChannels.add(channel.getUID().getId()));
97 Set<String> unusedHandlers = new HashSet<>(handlers.keySet());
99 Map<String, List<SmartHomeCapability>> capabilities = new HashMap<>();
100 getCapabilities(capabilities, accountHandler, smartHomeBaseDevice);
102 ThingBuilder thingBuilder = editThing();
104 for (String interfaceName : capabilities.keySet()) {
105 HandlerBase handler = handlers.get(interfaceName);
106 if (handler != null) {
107 unusedHandlers.remove(interfaceName);
109 Function<SmartHomeDeviceHandler, HandlerBase> creator = Constants.HANDLER_FACTORY.get(interfaceName);
110 if (creator != null) {
111 handler = creator.apply(this);
112 handlers.put(interfaceName, handler);
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)) {
128 unusedHandlers.forEach(handlers::remove);
129 if (!unusedChannels.isEmpty()) {
131 unusedChannels.stream().map(id -> new ChannelUID(thing.getUID(), id)).forEach(thingBuilder::withoutChannel);
135 updateThing(thingBuilder.build());
136 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Thing has changed.");
137 accountHandler.forceDelayedSmartHomeStateUpdate(getId());
141 public String getId() {
142 String id = (String) getConfig().get(DEVICE_PROPERTY_ID);
150 public void updateState(String channelId, State state) {
151 super.updateState(new ChannelUID(thing.getUID(), channelId), state);
155 public void initialize() {
156 AccountHandler accountHandler = getAccountHandler();
157 if (accountHandler != null) {
158 accountHandler.addSmartHomeDeviceHandler(this);
159 setDeviceAndUpdateThingState(accountHandler, smartHomeBaseDevice);
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridgehandler not found");
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
173 // channel exist with other settings, remove it first
174 thingBuilder.withoutChannel(channel.getUID());
176 thingBuilder.withChannel(ChannelBuilder.create(new ChannelUID(thing.getUID(), channelId), itemType)
177 .withType(channelTypeUID).build());
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!");
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) {
199 JsonArray states = applianceIdToCapabilityStates.get(applianceId);
200 if (states != null) {
202 if (smartHomeBaseDevice.isGroup()) {
203 // for groups, store the last state of all devices
204 lastStates.put(applianceId, states);
207 states = lastStates.get(applianceId);
208 if (states == null) {
212 if (firstDevice == null) {
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)
221 Objects.requireNonNull(mapInterfaceToStates.computeIfAbsent(interfaceName, k -> new ArrayList<>()))
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) {
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());
243 if (result.needSingleUpdate && smartHomeBaseDevice instanceof SmartHomeDevice && accountHandler != null) {
244 SmartHomeDevice shd = (SmartHomeDevice) smartHomeBaseDevice;
245 accountHandler.forceDelayedSmartHomeStateUpdate(shd.findId());
250 updateStatus(ThingStatus.ONLINE);
252 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "State not found");
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;
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());
275 Connection connection = accountHandler.findConnection();
276 if (connection == null) {
277 logger.debug("connection is null in {}", thing.getUID());
282 if (command instanceof RefreshType) {
283 accountHandler.forceDelayedSmartHomeStateUpdate(getId());
286 SmartHomeBaseDevice smartHomeBaseDevice = this.smartHomeBaseDevice;
287 if (smartHomeBaseDevice == null) {
288 logger.debug("smarthomeBaseDevice is null in {}", thing.getUID());
291 Set<SmartHomeDevice> devices = getSupportedSmartHomeDevices(smartHomeBaseDevice,
292 accountHandler.getLastKnownSmartHomeDevices());
293 String channelId = channelUID.getId();
295 for (String interfaceName : handlers.keySet()) {
296 HandlerBase handlerBase = handlers.get(interfaceName);
297 if (handlerBase == null || !handlerBase.hasChannel(channelId)) {
300 for (SmartHomeDevice shd : devices) {
301 String entityId = shd.entityId;
302 if (entityId == null) {
305 accountHandler.forceDelayedSmartHomeStateUpdate(getId()); // block updates
306 if (handlerBase.handleCommand(connection, shd, entityId, shd.getCapabilities(), channelUID.getId(),
308 accountHandler.forceDelayedSmartHomeStateUpdate(getId()); // force update again to restart
310 logger.debug("Command {} sent to {}", command, shd.findId());
314 } catch (Exception e) {
315 logger.warn("Handle command failed", e);
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<>()))
331 if (device instanceof SmartHomeGroup) {
332 for (SmartHomeDevice shd : getSupportedSmartHomeDevices(device,
333 accountHandler.getLastKnownSmartHomeDevices())) {
334 getCapabilities(result, accountHandler, shd);
339 public static Set<SmartHomeDevice> getSupportedSmartHomeDevices(@Nullable SmartHomeBaseDevice baseDevice,
340 List<SmartHomeBaseDevice> allDevices) {
341 if (baseDevice == null) {
342 return Collections.emptySet();
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)) {
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;
358 JsonSmartHomeGroupIdentity.SmartHomeGroupIdentity tagNameToValueSetMap = tags.tagNameToValueSetMap;
359 JsonSmartHomeGroupIdentifiers.SmartHomeGroupIdentifier applianceGroupIdentifier = shg.applianceGroupIdentifier;
360 if (tagNameToValueSetMap != null) {
361 List<String> groupIdentity = Objects.requireNonNullElse(tagNameToValueSetMap.groupIdentity,
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)) {
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);