2 * Copyright (c) 2010-2024 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.salus.internal.handler;
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;
24 import java.math.BigDecimal;
25 import java.math.MathContext;
26 import java.util.HashMap;
27 import java.util.List;
29 import java.util.Optional;
30 import java.util.SortedSet;
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;
56 * @author Martin GrzeĊlowski - Initial contribution
59 public class DeviceHandler extends BaseThingHandler {
60 private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
61 private final Logger logger;
65 private CloudApi cloudApi;
66 private final Map<String, String> channelUidMap = new HashMap<>();
67 private final Map<String, String> channelX100UidMap = new HashMap<>();
69 public DeviceHandler(Thing thing) {
71 logger = LoggerFactory.getLogger(DeviceHandler.class.getName() + "[" + thing.getUID().getId() + "]");
75 public void initialize() {
76 var bridge = getBridge();
78 updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/device-handler.initialize.errors.no-bridge");
81 var bridgeHandler = bridge.getHandler();
82 if (!(bridgeHandler instanceof CloudBridgeHandler cloudHandler)) {
83 updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/device-handler.initialize.errors.bridge-wrong-type");
86 this.cloudApi = cloudHandler;
88 dsn = (String) getConfig().get(DSN);
91 updateStatus(OFFLINE, CONFIGURATION_ERROR,
92 "@text/device-handler.initialize.errors.no-dsn [\"" + DSN + "\"]");
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 + "\"]");
103 if (!device.get().isConnected()) {
104 updateStatus(OFFLINE, COMMUNICATION_ERROR,
105 "@text/device-handler.initialize.errors.dsn-not-connected [\"" + dsn + "\"]");
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 + "\"]");
114 updateChannels(channels);
115 } catch (Exception e) {
116 updateStatus(OFFLINE, COMMUNICATION_ERROR, "@text/device-handler.initialize.errors.general-error");
121 updateStatus(ONLINE);
124 private Channel buildChannel(DeviceProperty<?> property) {
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())) {
134 channelId = inOrOut(property.getDirection(),
135 SalusBindingConstants.Channels.TEMPERATURE_INPUT_NUMBER_CHANNEL,
136 SalusBindingConstants.Channels.TEMPERATURE_OUTPUT_NUMBER_CHANNEL);
138 channelId = inOrOut(property.getDirection(),
139 SalusBindingConstants.Channels.GENERIC_INPUT_NUMBER_CHANNEL,
140 SalusBindingConstants.Channels.GENERIC_OUTPUT_NUMBER_CHANNEL);
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";
148 throw new UnsupportedOperationException(
149 "Property class " + property.getClass().getSimpleName() + " is not supported!");
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();
158 private String buildChannelUid(final String name) {
160 var map = channelUidMap;
161 if (name.contains("x100")) {
162 map = channelX100UidMap;
163 uid = removeX100(uid);
165 uid = uid.replaceAll("[^[\\w-]*]", "_");
166 final var firstUid = uid;
168 while (map.containsKey(uid)) {
169 uid = firstUid + "_" + idx++;
175 private String buildChannelDisplayName(final String displayName) {
176 if (displayName.contains("x100")) {
177 return removeX100(displayName);
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);
187 return withoutSuffix;
190 private String inOrOut(@Nullable String direction, String in, String out) {
191 if ("output".equalsIgnoreCase(direction)) {
194 if ("input".equalsIgnoreCase(direction)) {
198 logger.warn("Direction [{}] is unknown!", direction);
202 private void updateChannels(final List<Channel> channels) {
203 var thingBuilder = editThing();
204 thingBuilder.withChannels(channels);
205 updateThing(thingBuilder.build());
209 public void handleCommand(ChannelUID channelUID, Command command) {
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);
226 logger.warn("Does not know how to handle command `{}` ({}) on channel `{}`!", command,
227 command.getClass().getSimpleName(), channelUID);
229 } catch (SalusApiException e) {
230 logger.debug("Error while handling command `{}` on channel `{}`", command, channelUID, e);
231 updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
235 private void handleRefreshCommand(ChannelUID channelUID) throws SalusApiException {
236 var id = channelUID.getId();
239 if (channelUidMap.containsKey(id)) {
240 salusId = channelUidMap.get(id);
242 } else if (channelX100UidMap.containsKey(id)) {
243 salusId = channelX100UidMap.get(id);
246 logger.warn("Channel {} not found in channelUidMap and channelX100UidMap!", id);
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);
256 var property = propertyOptional.get();
258 if (property instanceof DeviceProperty.BooleanDeviceProperty booleanProperty) {
259 var value = booleanProperty.getValue();
260 if (value != null && value) {
261 state = OnOffType.ON;
263 state = OnOffType.OFF;
265 } else if (property instanceof DeviceProperty.LongDeviceProperty longDeviceProperty) {
266 var value = longDeviceProperty.getValue();
271 state = new DecimalType(new BigDecimal(value).divide(ONE_HUNDRED, new MathContext(5, HALF_EVEN)));
273 state = new DecimalType(value);
275 } else if (property instanceof DeviceProperty.StringDeviceProperty stringDeviceProperty) {
276 state = new StringType(stringDeviceProperty.getValue());
278 logger.warn("Property class {} is not supported!", property.getClass().getSimpleName());
281 updateState(channelUID, state);
284 private SortedSet<DeviceProperty<?>> findDeviceProperties() throws SalusApiException {
285 return this.cloudApi.findPropertiesForDevice(dsn);
288 private void handleBoolCommand(ChannelUID channelUID, boolean command) throws SalusApiException {
289 var id = channelUID.getId();
291 if (channelUidMap.containsKey(id)) {
292 salusId = requireNonNull(channelUidMap.get(id));
294 logger.warn("Channel {} not found in channelUidMap!", id);
297 cloudApi.setValueForProperty(dsn, salusId, command);
298 handleCommand(channelUID, REFRESH);
301 private void handleDecimalCommand(ChannelUID channelUID, @Nullable DecimalType command) throws SalusApiException {
302 if (command == null) {
305 var id = channelUID.getId();
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();
315 logger.warn("Channel {} not found in channelUidMap and channelX100UidMap!", id);
318 cloudApi.setValueForProperty(dsn, salusId, value);
319 handleCommand(channelUID, REFRESH);
322 private void handleStringCommand(ChannelUID channelUID, StringType command) throws SalusApiException {
323 var id = channelUID.getId();
325 if (channelUidMap.containsKey(id)) {
326 salusId = requireNonNull(channelUidMap.get(id));
328 logger.warn("Channel {} not found in channelUidMap!", id);
331 var value = command.toFullString();
332 cloudApi.setValueForProperty(dsn, salusId, value);
333 handleCommand(channelUID, REFRESH);