]> git.basschouten.com Git - openhab-addons.git/blob
d54ebcfb873ba60298575f51252b86d23759b22b
[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.salus.internal.handler;
14
15 import static java.math.RoundingMode.HALF_EVEN;
16 import static java.util.Objects.requireNonNull;
17 import static org.openhab.binding.salus.internal.SalusBindingConstants.BINDING_ID;
18 import static org.openhab.binding.salus.internal.SalusBindingConstants.SalusDevice.DSN;
19 import static org.openhab.core.thing.ThingStatus.OFFLINE;
20 import static org.openhab.core.thing.ThingStatus.ONLINE;
21 import static org.openhab.core.thing.ThingStatusDetail.*;
22 import static org.openhab.core.types.RefreshType.REFRESH;
23
24 import java.math.BigDecimal;
25 import java.math.MathContext;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Optional;
30 import java.util.SortedSet;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.salus.internal.SalusBindingConstants;
35 import org.openhab.binding.salus.internal.rest.DeviceProperty;
36 import org.openhab.binding.salus.internal.rest.SalusApiException;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.OpenClosedType;
40 import org.openhab.core.library.types.PercentType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.library.types.UpDownType;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.builder.ChannelBuilder;
48 import org.openhab.core.thing.type.ChannelTypeUID;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.openhab.core.types.State;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * @author Martin GrzeĊ›lowski - Initial contribution
57  */
58 @NonNullByDefault
59 public class DeviceHandler extends BaseThingHandler {
60     private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
61     private final Logger logger;
62     @NonNullByDefault({})
63     private String dsn;
64     @NonNullByDefault({})
65     private CloudApi cloudApi;
66     private final Map<String, String> channelUidMap = new HashMap<>();
67     private final Map<String, String> channelX100UidMap = new HashMap<>();
68
69     public DeviceHandler(Thing thing) {
70         super(thing);
71         logger = LoggerFactory.getLogger(DeviceHandler.class.getName() + "[" + thing.getUID().getId() + "]");
72     }
73
74     @Override
75     public void initialize() {
76         var bridge = getBridge();
77         if (bridge == null) {
78             updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/device-handler.initialize.errors.no-bridge");
79             return;
80         }
81         var bridgeHandler = bridge.getHandler();
82         if (!(bridgeHandler instanceof CloudBridgeHandler cloudHandler)) {
83             updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/device-handler.initialize.errors.bridge-wrong-type");
84             return;
85         }
86         this.cloudApi = cloudHandler;
87
88         dsn = (String) getConfig().get(DSN);
89
90         if ("".equals(dsn)) {
91             updateStatus(OFFLINE, CONFIGURATION_ERROR,
92                     "@text/device-handler.initialize.errors.no-dsn [\"" + DSN + "\"]");
93             return;
94         }
95
96         try {
97             var device = this.cloudApi.findDevice(dsn);
98             if (device.isEmpty()) {
99                 updateStatus(OFFLINE, COMMUNICATION_ERROR,
100                         "@text/device-handler.initialize.errors.dsn-not-found [\"" + dsn + "\"]");
101                 return;
102             }
103             if (!device.get().isConnected()) {
104                 updateStatus(OFFLINE, COMMUNICATION_ERROR,
105                         "@text/device-handler.initialize.errors.dsn-not-connected [\"" + dsn + "\"]");
106                 return;
107             }
108             var channels = findDeviceProperties().stream().map(this::buildChannel).toList();
109             if (channels.isEmpty()) {
110                 updateStatus(OFFLINE, CONFIGURATION_ERROR,
111                         "@text/device-handler.initialize.errors.no-channels [\"" + dsn + "\"]");
112                 return;
113             }
114             updateChannels(channels);
115         } catch (Exception e) {
116             updateStatus(OFFLINE, COMMUNICATION_ERROR, "@text/device-handler.initialize.errors.general-error");
117             return;
118         }
119
120         // done
121         updateStatus(ONLINE);
122     }
123
124     private Channel buildChannel(DeviceProperty<?> property) {
125         String channelId;
126         String acceptedItemType;
127         if (property instanceof DeviceProperty.BooleanDeviceProperty) {
128             channelId = inOrOut(property.getDirection(), SalusBindingConstants.Channels.GENERIC_INPUT_BOOL_CHANNEL,
129                     SalusBindingConstants.Channels.GENERIC_OUTPUT_BOOL_CHANNEL);
130             acceptedItemType = "Switch";
131         } else if (property instanceof DeviceProperty.LongDeviceProperty longDeviceProperty) {
132             if (SalusBindingConstants.Channels.TEMPERATURE_CHANNELS.contains(longDeviceProperty.getName())) {
133                 // a temp channel
134                 channelId = inOrOut(property.getDirection(),
135                         SalusBindingConstants.Channels.TEMPERATURE_INPUT_NUMBER_CHANNEL,
136                         SalusBindingConstants.Channels.TEMPERATURE_OUTPUT_NUMBER_CHANNEL);
137             } else {
138                 channelId = inOrOut(property.getDirection(),
139                         SalusBindingConstants.Channels.GENERIC_INPUT_NUMBER_CHANNEL,
140                         SalusBindingConstants.Channels.GENERIC_OUTPUT_NUMBER_CHANNEL);
141             }
142             acceptedItemType = "Number";
143         } else if (property instanceof DeviceProperty.StringDeviceProperty) {
144             channelId = inOrOut(property.getDirection(), SalusBindingConstants.Channels.GENERIC_INPUT_CHANNEL,
145                     SalusBindingConstants.Channels.GENERIC_OUTPUT_CHANNEL);
146             acceptedItemType = "String";
147         } else {
148             throw new UnsupportedOperationException(
149                     "Property class " + property.getClass().getSimpleName() + " is not supported!");
150         }
151
152         var channelUid = new ChannelUID(thing.getUID(), buildChannelUid(property.getName()));
153         var channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
154         return ChannelBuilder.create(channelUid, acceptedItemType).withType(channelTypeUID)
155                 .withLabel(buildChannelDisplayName(property.getDisplayName())).build();
156     }
157
158     private String buildChannelUid(final String name) {
159         String uid = name;
160         var map = channelUidMap;
161         if (name.contains("x100")) {
162             map = channelX100UidMap;
163             uid = removeX100(uid);
164         }
165         uid = uid.replaceAll("[^[\\w-]*]", "_");
166         final var firstUid = uid;
167         var idx = 1;
168         while (map.containsKey(uid)) {
169             uid = firstUid + "_" + idx++;
170         }
171         map.put(uid, name);
172         return uid;
173     }
174
175     private String buildChannelDisplayName(final String displayName) {
176         if (displayName.contains("x100")) {
177             return removeX100(displayName);
178         }
179         return displayName;
180     }
181
182     private static String removeX100(String name) {
183         var withoutSuffix = name.replace("_x100", "").replace("x100", "");
184         if (withoutSuffix.endsWith("_")) {
185             withoutSuffix = withoutSuffix.substring(0, withoutSuffix.length() - 2);
186         }
187         return withoutSuffix;
188     }
189
190     private String inOrOut(@Nullable String direction, String in, String out) {
191         if ("output".equalsIgnoreCase(direction)) {
192             return out;
193         }
194         if ("input".equalsIgnoreCase(direction)) {
195             return in;
196         }
197
198         logger.warn("Direction [{}] is unknown!", direction);
199         return out;
200     }
201
202     private void updateChannels(final List<Channel> channels) {
203         var thingBuilder = editThing();
204         thingBuilder.withChannels(channels);
205         updateThing(thingBuilder.build());
206     }
207
208     @Override
209     public void handleCommand(ChannelUID channelUID, Command command) {
210         try {
211             if (command instanceof RefreshType) {
212                 handleRefreshCommand(channelUID);
213             } else if (command instanceof OnOffType typedCommand) {
214                 handleBoolCommand(channelUID, typedCommand == OnOffType.ON);
215             } else if (command instanceof UpDownType typedCommand) {
216                 handleBoolCommand(channelUID, typedCommand == UpDownType.UP);
217             } else if (command instanceof OpenClosedType typedCommand) {
218                 handleBoolCommand(channelUID, typedCommand == OpenClosedType.OPEN);
219             } else if (command instanceof PercentType typedCommand) {
220                 handleDecimalCommand(channelUID, typedCommand.as(DecimalType.class));
221             } else if (command instanceof DecimalType typedCommand) {
222                 handleDecimalCommand(channelUID, typedCommand);
223             } else if (command instanceof StringType typedCommand) {
224                 handleStringCommand(channelUID, typedCommand);
225             } else {
226                 logger.warn("Does not know how to handle command `{}` ({}) on channel `{}`!", command,
227                         command.getClass().getSimpleName(), channelUID);
228             }
229         } catch (SalusApiException e) {
230             logger.debug("Error while handling command `{}` on channel `{}`", command, channelUID, e);
231             updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
232         }
233     }
234
235     private void handleRefreshCommand(ChannelUID channelUID) throws SalusApiException {
236         var id = channelUID.getId();
237         String salusId;
238         boolean isX100;
239         if (channelUidMap.containsKey(id)) {
240             salusId = channelUidMap.get(id);
241             isX100 = false;
242         } else if (channelX100UidMap.containsKey(id)) {
243             salusId = channelX100UidMap.get(id);
244             isX100 = true;
245         } else {
246             logger.warn("Channel {} not found in channelUidMap and channelX100UidMap!", id);
247             return;
248         }
249
250         Optional<DeviceProperty<?>> propertyOptional = findDeviceProperties().stream()
251                 .filter(property -> property.getName().equals(salusId)).findFirst();
252         if (propertyOptional.isEmpty()) {
253             logger.warn("Property {} not found in response!", salusId);
254             return;
255         }
256         var property = propertyOptional.get();
257         State state;
258         if (property instanceof DeviceProperty.BooleanDeviceProperty booleanProperty) {
259             var value = booleanProperty.getValue();
260             if (value != null && value) {
261                 state = OnOffType.ON;
262             } else {
263                 state = OnOffType.OFF;
264             }
265         } else if (property instanceof DeviceProperty.LongDeviceProperty longDeviceProperty) {
266             var value = longDeviceProperty.getValue();
267             if (value == null) {
268                 value = 0L;
269             }
270             if (isX100) {
271                 state = new DecimalType(new BigDecimal(value).divide(ONE_HUNDRED, new MathContext(5, HALF_EVEN)));
272             } else {
273                 state = new DecimalType(value);
274             }
275         } else if (property instanceof DeviceProperty.StringDeviceProperty stringDeviceProperty) {
276             state = new StringType(stringDeviceProperty.getValue());
277         } else {
278             logger.warn("Property class {} is not supported!", property.getClass().getSimpleName());
279             return;
280         }
281         updateState(channelUID, state);
282     }
283
284     private SortedSet<DeviceProperty<?>> findDeviceProperties() throws SalusApiException {
285         return this.cloudApi.findPropertiesForDevice(dsn);
286     }
287
288     private void handleBoolCommand(ChannelUID channelUID, boolean command) throws SalusApiException {
289         var id = channelUID.getId();
290         String salusId;
291         if (channelUidMap.containsKey(id)) {
292             salusId = requireNonNull(channelUidMap.get(id));
293         } else {
294             logger.warn("Channel {} not found in channelUidMap!", id);
295             return;
296         }
297         cloudApi.setValueForProperty(dsn, salusId, command);
298         handleCommand(channelUID, REFRESH);
299     }
300
301     private void handleDecimalCommand(ChannelUID channelUID, @Nullable DecimalType command) throws SalusApiException {
302         if (command == null) {
303             return;
304         }
305         var id = channelUID.getId();
306         String salusId;
307         long value;
308         if (channelUidMap.containsKey(id)) {
309             salusId = requireNonNull(channelUidMap.get(id));
310             value = command.toBigDecimal().longValue();
311         } else if (channelX100UidMap.containsKey(id)) {
312             salusId = requireNonNull(channelX100UidMap.get(id));
313             value = command.toBigDecimal().multiply(ONE_HUNDRED).longValue();
314         } else {
315             logger.warn("Channel {} not found in channelUidMap and channelX100UidMap!", id);
316             return;
317         }
318         cloudApi.setValueForProperty(dsn, salusId, value);
319         handleCommand(channelUID, REFRESH);
320     }
321
322     private void handleStringCommand(ChannelUID channelUID, StringType command) throws SalusApiException {
323         var id = channelUID.getId();
324         String salusId;
325         if (channelUidMap.containsKey(id)) {
326             salusId = requireNonNull(channelUidMap.get(id));
327         } else {
328             logger.warn("Channel {} not found in channelUidMap!", id);
329             return;
330         }
331         var value = command.toFullString();
332         cloudApi.setValueForProperty(dsn, salusId, value);
333         handleCommand(channelUID, REFRESH);
334     }
335 }