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.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;
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;
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;
48 * @author Martin GrzeĊlowski - Initial contribution
51 public class It600Handler extends BaseThingHandler {
52 private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
53 private final Logger logger;
57 private CloudApi cloudApi;
58 private String channelPrefix = "";
60 public It600Handler(Thing thing) {
62 logger = LoggerFactory.getLogger(It600Handler.class.getName() + "[" + thing.getUID().getId() + "]");
66 public void initialize() {
67 AbstractBridgeHandler<?> abstractBridgeHandler;
69 var bridge = getBridge();
71 updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/it600-handler.initialize.errors.no-bridge");
74 if (!(bridge.getHandler() instanceof AbstractBridgeHandler<?> cloudHandler)) {
75 updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/it600-handler.initialize.errors.bridge-wrong-type");
78 this.cloudApi = cloudHandler;
79 abstractBridgeHandler = cloudHandler;
80 channelPrefix = abstractBridgeHandler.channelPrefix();
83 dsn = (String) getConfig().get(DSN);
86 updateStatus(OFFLINE, CONFIGURATION_ERROR,
87 "@text/it600-handler.initialize.errors.no-dsn [\"" + DSN + "\"]");
92 var device = this.cloudApi.findDevice(dsn);
94 if (device.isEmpty()) {
95 updateStatus(OFFLINE, COMMUNICATION_ERROR,
96 "@text/it600-handler.initialize.errors.dsn-not-found [\"" + dsn + "\"]");
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 + "\"]");
105 // device is missing properties
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) + "\"]");
116 } catch (SalusApiException ex) {
117 updateStatus(OFFLINE, COMMUNICATION_ERROR, ex.getLocalizedMessage());
120 } catch (Exception e) {
121 updateStatus(OFFLINE, COMMUNICATION_ERROR, "@text/it600-handler.initialize.errors.general-error");
126 updateStatus(ONLINE);
130 public void handleCommand(ChannelUID channelUID, Command command) {
131 if (command != REFRESH && cloudApi.isReadOnly()) {
135 var id = channelUID.getId();
138 handleCommandForTemperature(channelUID, command);
140 case EXPECTED_TEMPERATURE:
141 handleCommandForExpectedTemperature(channelUID, command);
144 handleCommandForWorkType(channelUID, command);
147 logger.warn("Unknown channel `{}` for command `{}`", id, command);
149 } catch (SalusApiException | AuthSalusApiException e) {
150 logger.debug("Error while handling command `{}` on channel `{}`", command, channelUID, e);
151 updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
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
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);
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);
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();
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()) {
198 var wasSet = cloudApi.setValueForProperty(dsn, property.get().getName(), value);
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);
209 logger.debug("Does not know how to handle command `{}` ({}) on channel `{}`!", command,
210 command.getClass().getSimpleName(), channelUID);
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()) {
219 case MANUAL -> "MANUAL";
220 case TEMPORARY_MANUAL -> "TEMPORARY_MANUAL";
223 logger.warn("Unknown value {} for property HoldType!", value);
226 }).map(StringType::new).ifPresent(state -> {
227 updateState(channelUID, state);
228 updateStatus(ONLINE);
233 if (command instanceof StringType typedCommand) {
235 if ("AUTO".equals(typedCommand.toString())) {
237 } else if ("MANUAL".equals(typedCommand.toString())) {
239 } else if ("TEMPORARY_MANUAL".equals(typedCommand.toString())) {
240 value = TEMPORARY_MANUAL;
241 } else if ("OFF".equals(typedCommand.toString())) {
244 logger.warn("Unknown value `{}` for property HoldType!", typedCommand);
247 var property = findLongProperty(channelPrefix + ":sIT600TH:SetHoldType", "SetHoldType");
248 if (property.isEmpty()) {
251 cloudApi.setValueForProperty(dsn, property.get().getName(), value);
252 updateStatus(ONLINE);
256 logger.debug("Does not know how to handle command `{}` ({}) on channel `{}`!", command,
257 command.getClass().getSimpleName(), channelUID);
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();
271 if (property.isEmpty()) {
272 logger.debug("{}/{} property not found!", name, shortName);
277 private SortedSet<DeviceProperty<?>> findDeviceProperties() throws SalusApiException, AuthSalusApiException {
278 return this.cloudApi.findPropertiesForDevice(dsn);