]> git.basschouten.com Git - openhab-addons.git/blob
d6bb34b3a60ea95c257b43c96ec07f47b7714c88
[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.Channels.It600.*;
18 import static org.openhab.binding.salus.internal.SalusBindingConstants.It600Device.HoldType.*;
19 import static org.openhab.binding.salus.internal.SalusBindingConstants.SalusDevice.DSN;
20 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
21 import static org.openhab.core.thing.ThingStatus.OFFLINE;
22 import static org.openhab.core.thing.ThingStatus.ONLINE;
23 import static org.openhab.core.thing.ThingStatusDetail.*;
24 import static org.openhab.core.types.RefreshType.REFRESH;
25
26 import java.math.BigDecimal;
27 import java.math.MathContext;
28 import java.util.ArrayList;
29 import java.util.Optional;
30 import java.util.SortedSet;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.openhab.binding.salus.internal.rest.DeviceProperty;
34 import org.openhab.binding.salus.internal.rest.exceptions.AuthSalusApiException;
35 import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.QuantityType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * @author Martin GrzeĊ›lowski - Initial contribution
49  */
50 @NonNullByDefault
51 public class It600Handler extends BaseThingHandler {
52     private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
53     private final Logger logger;
54     @NonNullByDefault({})
55     private String dsn;
56     @NonNullByDefault({})
57     private CloudApi cloudApi;
58     private String channelPrefix = "";
59
60     public It600Handler(Thing thing) {
61         super(thing);
62         logger = LoggerFactory.getLogger(It600Handler.class.getName() + "[" + thing.getUID().getId() + "]");
63     }
64
65     @Override
66     public void initialize() {
67         AbstractBridgeHandler<?> abstractBridgeHandler;
68         {
69             var bridge = getBridge();
70             if (bridge == null) {
71                 updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/it600-handler.initialize.errors.no-bridge");
72                 return;
73             }
74             if (!(bridge.getHandler() instanceof AbstractBridgeHandler<?> cloudHandler)) {
75                 updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/it600-handler.initialize.errors.bridge-wrong-type");
76                 return;
77             }
78             this.cloudApi = cloudHandler;
79             abstractBridgeHandler = cloudHandler;
80             channelPrefix = abstractBridgeHandler.channelPrefix();
81         }
82
83         dsn = (String) getConfig().get(DSN);
84
85         if ("".equals(dsn)) {
86             updateStatus(OFFLINE, CONFIGURATION_ERROR,
87                     "@text/it600-handler.initialize.errors.no-dsn [\"" + DSN + "\"]");
88             return;
89         }
90
91         try {
92             var device = this.cloudApi.findDevice(dsn);
93             // no device in cloud
94             if (device.isEmpty()) {
95                 updateStatus(OFFLINE, COMMUNICATION_ERROR,
96                         "@text/it600-handler.initialize.errors.dsn-not-found [\"" + dsn + "\"]");
97                 return;
98             }
99             // device is not connected
100             if (!device.get().connected()) {
101                 updateStatus(OFFLINE, COMMUNICATION_ERROR,
102                         "@text/it600-handler.initialize.errors.dsn-not-connected [\"" + dsn + "\"]");
103                 return;
104             }
105             // device is missing properties
106             try {
107                 var deviceProperties = findDeviceProperties().stream().map(DeviceProperty::getName).toList();
108                 var result = new ArrayList<>(abstractBridgeHandler.it600RequiredChannels());
109                 result.removeAll(deviceProperties);
110                 if (!result.isEmpty()) {
111                     updateStatus(OFFLINE, CONFIGURATION_ERROR,
112                             "@text/it600-handler.initialize.errors.missing-channels [\"" + dsn + "\", \""
113                                     + String.join(", ", result) + "\"]");
114                     return;
115                 }
116             } catch (SalusApiException ex) {
117                 updateStatus(OFFLINE, COMMUNICATION_ERROR, ex.getLocalizedMessage());
118                 return;
119             }
120         } catch (Exception e) {
121             updateStatus(OFFLINE, COMMUNICATION_ERROR, "@text/it600-handler.initialize.errors.general-error");
122             return;
123         }
124
125         // done
126         updateStatus(ONLINE);
127     }
128
129     @Override
130     public void handleCommand(ChannelUID channelUID, Command command) {
131         if (command != REFRESH && cloudApi.isReadOnly()) {
132             return;
133         }
134         try {
135             var id = channelUID.getId();
136             switch (id) {
137                 case TEMPERATURE:
138                     handleCommandForTemperature(channelUID, command);
139                     break;
140                 case EXPECTED_TEMPERATURE:
141                     handleCommandForExpectedTemperature(channelUID, command);
142                     break;
143                 case WORK_TYPE:
144                     handleCommandForWorkType(channelUID, command);
145                     break;
146                 default:
147                     logger.warn("Unknown channel `{}` for command `{}`", id, command);
148             }
149         } catch (SalusApiException | AuthSalusApiException e) {
150             logger.debug("Error while handling command `{}` on channel `{}`", command, channelUID, e);
151             updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
152         }
153     }
154
155     private void handleCommandForTemperature(ChannelUID channelUID, Command command)
156             throws SalusApiException, AuthSalusApiException {
157         if (!(command instanceof RefreshType)) {
158             // only refresh commands are supported for temp channel
159             return;
160         }
161
162         findLongProperty(channelPrefix + ":sIT600TH:LocalTemperature_x100", "LocalTemperature_x100")
163                 .map(DeviceProperty.LongDeviceProperty::getValue).map(BigDecimal::new)
164                 .map(value -> value.divide(ONE_HUNDRED, new MathContext(5, HALF_EVEN))).map(DecimalType::new)
165                 .ifPresent(state -> {
166                     updateState(channelUID, state);
167                     updateStatus(ONLINE);
168                 });
169     }
170
171     private void handleCommandForExpectedTemperature(ChannelUID channelUID, Command command)
172             throws SalusApiException, AuthSalusApiException {
173         if (command instanceof RefreshType) {
174             findLongProperty(channelPrefix + ":sIT600TH:HeatingSetpoint_x100", "HeatingSetpoint_x100")
175                     .map(DeviceProperty.LongDeviceProperty::getValue).map(BigDecimal::new)
176                     .map(value -> value.divide(ONE_HUNDRED, new MathContext(5, HALF_EVEN))).map(DecimalType::new)
177                     .ifPresent(state -> {
178                         updateState(channelUID, state);
179                         updateStatus(ONLINE);
180                     });
181             return;
182         }
183
184         BigDecimal rawValue = null;
185         if (command instanceof QuantityType<?> commandAsQuantityType) {
186             rawValue = requireNonNull(commandAsQuantityType.toUnit(CELSIUS)).toBigDecimal();
187         } else if (command instanceof DecimalType commandAsDecimalType) {
188             rawValue = commandAsDecimalType.toBigDecimal();
189         }
190
191         if (rawValue != null) {
192             var value = rawValue.multiply(ONE_HUNDRED).longValue();
193             var property = findLongProperty(channelPrefix + ":sIT600TH:SetHeatingSetpoint_x100",
194                     "SetHeatingSetpoint_x100");
195             if (property.isEmpty()) {
196                 return;
197             }
198             var wasSet = cloudApi.setValueForProperty(dsn, property.get().getName(), value);
199             if (wasSet) {
200                 findLongProperty(channelPrefix + ":sIT600TH:HeatingSetpoint_x100", "HeatingSetpoint_x100")
201                         .ifPresent(prop -> prop.setValue(value));
202                 findLongProperty(channelPrefix + ":sIT600TH:HoldType", "HoldType")
203                         .ifPresent(prop -> prop.setValue((long) MANUAL));
204                 updateStatus(ONLINE);
205             }
206             return;
207         }
208
209         logger.debug("Does not know how to handle command `{}` ({}) on channel `{}`!", command,
210                 command.getClass().getSimpleName(), channelUID);
211     }
212
213     private void handleCommandForWorkType(ChannelUID channelUID, Command command)
214             throws SalusApiException, AuthSalusApiException {
215         if (command instanceof RefreshType) {
216             findLongProperty(channelPrefix + ":sIT600TH:HoldType", "HoldType")
217                     .map(DeviceProperty.LongDeviceProperty::getValue).map(value -> switch (value.intValue()) {
218                         case AUTO -> "AUTO";
219                         case MANUAL -> "MANUAL";
220                         case TEMPORARY_MANUAL -> "TEMPORARY_MANUAL";
221                         case OFF -> "OFF";
222                         default -> {
223                             logger.warn("Unknown value {} for property HoldType!", value);
224                             yield "AUTO";
225                         }
226                     }).map(StringType::new).ifPresent(state -> {
227                         updateState(channelUID, state);
228                         updateStatus(ONLINE);
229                     });
230             return;
231         }
232
233         if (command instanceof StringType typedCommand) {
234             long value;
235             if ("AUTO".equals(typedCommand.toString())) {
236                 value = AUTO;
237             } else if ("MANUAL".equals(typedCommand.toString())) {
238                 value = MANUAL;
239             } else if ("TEMPORARY_MANUAL".equals(typedCommand.toString())) {
240                 value = TEMPORARY_MANUAL;
241             } else if ("OFF".equals(typedCommand.toString())) {
242                 value = OFF;
243             } else {
244                 logger.warn("Unknown value `{}` for property HoldType!", typedCommand);
245                 return;
246             }
247             var property = findLongProperty(channelPrefix + ":sIT600TH:SetHoldType", "SetHoldType");
248             if (property.isEmpty()) {
249                 return;
250             }
251             cloudApi.setValueForProperty(dsn, property.get().getName(), value);
252             updateStatus(ONLINE);
253             return;
254         }
255
256         logger.debug("Does not know how to handle command `{}` ({}) on channel `{}`!", command,
257                 command.getClass().getSimpleName(), channelUID);
258     }
259
260     private Optional<DeviceProperty.LongDeviceProperty> findLongProperty(String name, String shortName)
261             throws SalusApiException, AuthSalusApiException {
262         var deviceProperties = findDeviceProperties();
263         var property = deviceProperties.stream().filter(p -> p.getName().equals(name))
264                 .filter(DeviceProperty.LongDeviceProperty.class::isInstance)
265                 .map(DeviceProperty.LongDeviceProperty.class::cast).findAny();
266         if (property.isEmpty()) {
267             property = deviceProperties.stream().filter(p -> p.getName().contains(shortName))
268                     .filter(DeviceProperty.LongDeviceProperty.class::isInstance)
269                     .map(DeviceProperty.LongDeviceProperty.class::cast).findAny();
270         }
271         if (property.isEmpty()) {
272             logger.debug("{}/{} property not found!", name, shortName);
273         }
274         return property;
275     }
276
277     private SortedSet<DeviceProperty<?>> findDeviceProperties() throws SalusApiException, AuthSalusApiException {
278         return this.cloudApi.findPropertiesForDevice(dsn);
279     }
280 }