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.EXPECTED_TEMPERATURE;
18 import static org.openhab.binding.salus.internal.SalusBindingConstants.Channels.It600.TEMPERATURE;
19 import static org.openhab.binding.salus.internal.SalusBindingConstants.Channels.It600.WORK_TYPE;
20 import static org.openhab.binding.salus.internal.SalusBindingConstants.It600Device.HoldType.AUTO;
21 import static org.openhab.binding.salus.internal.SalusBindingConstants.It600Device.HoldType.MANUAL;
22 import static org.openhab.binding.salus.internal.SalusBindingConstants.It600Device.HoldType.OFF;
23 import static org.openhab.binding.salus.internal.SalusBindingConstants.It600Device.HoldType.TEMPORARY_MANUAL;
24 import static org.openhab.binding.salus.internal.SalusBindingConstants.SalusDevice.DSN;
25 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
26 import static org.openhab.core.thing.ThingStatus.OFFLINE;
27 import static org.openhab.core.thing.ThingStatus.ONLINE;
28 import static org.openhab.core.thing.ThingStatusDetail.BRIDGE_UNINITIALIZED;
29 import static org.openhab.core.thing.ThingStatusDetail.COMMUNICATION_ERROR;
30 import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
32 import java.math.BigDecimal;
33 import java.math.MathContext;
34 import java.util.ArrayList;
35 import java.util.Optional;
37 import java.util.SortedSet;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.openhab.binding.salus.internal.rest.DeviceProperty;
41 import org.openhab.binding.salus.internal.rest.SalusApiException;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.QuantityType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * @author Martin GrzeĊlowski - Initial contribution
57 public class It600Handler extends BaseThingHandler {
58 private static final BigDecimal ONE_HUNDRED = new BigDecimal(100);
59 private static final Set<String> REQUIRED_CHANNELS = Set.of("ep_9:sIT600TH:LocalTemperature_x100",
60 "ep_9:sIT600TH:HeatingSetpoint_x100", "ep_9:sIT600TH:SetHeatingSetpoint_x100", "ep_9:sIT600TH:HoldType",
61 "ep_9:sIT600TH:SetHoldType");
62 private final Logger logger;
66 private CloudApi cloudApi;
68 public It600Handler(Thing thing) {
70 logger = LoggerFactory.getLogger(It600Handler.class.getName() + "[" + thing.getUID().getId() + "]");
74 public void initialize() {
76 var bridge = getBridge();
78 updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/it600-handler.initialize.errors.no-bridge");
81 if (!(bridge.getHandler() instanceof CloudBridgeHandler cloudHandler)) {
82 updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "@text/it600-handler.initialize.errors.bridge-wrong-type");
85 this.cloudApi = cloudHandler;
88 dsn = (String) getConfig().get(DSN);
91 updateStatus(OFFLINE, CONFIGURATION_ERROR,
92 "@text/it600-handler.initialize.errors.no-dsn [\"" + DSN + "\"]");
97 var device = this.cloudApi.findDevice(dsn);
99 if (device.isEmpty()) {
100 updateStatus(OFFLINE, COMMUNICATION_ERROR,
101 "@text/it600-handler.initialize.errors.dsn-not-found [\"" + dsn + "\"]");
104 // device is not connected
105 if (!device.get().isConnected()) {
106 updateStatus(OFFLINE, COMMUNICATION_ERROR,
107 "@text/it600-handler.initialize.errors.dsn-not-connected [\"" + dsn + "\"]");
110 // device is missing properties
112 var deviceProperties = findDeviceProperties().stream().map(DeviceProperty::getName).toList();
113 var result = new ArrayList<>(REQUIRED_CHANNELS);
114 result.removeAll(deviceProperties);
115 if (!result.isEmpty()) {
116 updateStatus(OFFLINE, CONFIGURATION_ERROR,
117 "@text/it600-handler.initialize.errors.missing-channels [\"" + dsn + "\", \""
118 + String.join(", ", result) + "\"]");
121 } catch (SalusApiException ex) {
122 updateStatus(OFFLINE, COMMUNICATION_ERROR, ex.getLocalizedMessage());
125 } catch (Exception e) {
126 updateStatus(OFFLINE, COMMUNICATION_ERROR, "@text/it600-handler.initialize.errors.general-error");
131 updateStatus(ONLINE);
135 public void handleCommand(ChannelUID channelUID, Command command) {
137 var id = channelUID.getId();
140 handleCommandForTemperature(channelUID, command);
142 case EXPECTED_TEMPERATURE:
143 handleCommandForExpectedTemperature(channelUID, command);
146 handleCommandForWorkType(channelUID, command);
149 logger.warn("Unknown channel `{}` for command `{}`", id, command);
151 } catch (SalusApiException e) {
152 logger.debug("Error while handling command `{}` on channel `{}`", command, channelUID, e);
153 updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
157 private void handleCommandForTemperature(ChannelUID channelUID, Command command) throws SalusApiException {
158 if (!(command instanceof RefreshType)) {
159 // only refresh commands are supported for temp channel
163 findLongProperty("ep_9:sIT600TH:LocalTemperature_x100", "LocalTemperature_x100")
164 .map(DeviceProperty.LongDeviceProperty::getValue).map(BigDecimal::new)
165 .map(value -> value.divide(ONE_HUNDRED, new MathContext(5, HALF_EVEN))).map(DecimalType::new)
166 .ifPresent(state -> {
167 updateState(channelUID, state);
168 updateStatus(ONLINE);
172 private void handleCommandForExpectedTemperature(ChannelUID channelUID, Command command) throws SalusApiException {
173 if (command instanceof RefreshType) {
174 findLongProperty("ep_9: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("ep_9:sIT600TH:SetHeatingSetpoint_x100", "SetHeatingSetpoint_x100");
194 if (property.isEmpty()) {
197 var wasSet = cloudApi.setValueForProperty(dsn, property.get().getName(), value);
199 findLongProperty("ep_9:sIT600TH:HeatingSetpoint_x100", "HeatingSetpoint_x100")
200 .ifPresent(prop -> prop.setValue(value));
201 findLongProperty("ep_9:sIT600TH:HoldType", "HoldType").ifPresent(prop -> prop.setValue((long) MANUAL));
202 updateStatus(ONLINE);
207 logger.debug("Does not know how to handle command `{}` ({}) on channel `{}`!", command,
208 command.getClass().getSimpleName(), channelUID);
211 private void handleCommandForWorkType(ChannelUID channelUID, Command command) throws SalusApiException {
212 if (command instanceof RefreshType) {
213 findLongProperty("ep_9:sIT600TH:HoldType", "HoldType").map(DeviceProperty.LongDeviceProperty::getValue)
214 .map(value -> switch (value.intValue()) {
216 case MANUAL -> "MANUAL";
217 case TEMPORARY_MANUAL -> "TEMPORARY_MANUAL";
220 logger.warn("Unknown value {} for property HoldType!", value);
223 }).map(StringType::new).ifPresent(state -> {
224 updateState(channelUID, state);
225 updateStatus(ONLINE);
230 if (command instanceof StringType typedCommand) {
232 if ("AUTO".equals(typedCommand.toString())) {
234 } else if ("MANUAL".equals(typedCommand.toString())) {
236 } else if ("TEMPORARY_MANUAL".equals(typedCommand.toString())) {
237 value = TEMPORARY_MANUAL;
238 } else if ("OFF".equals(typedCommand.toString())) {
241 logger.warn("Unknown value `{}` for property HoldType!", typedCommand);
244 var property = findLongProperty("ep_9:sIT600TH:SetHoldType", "SetHoldType");
245 if (property.isEmpty()) {
248 cloudApi.setValueForProperty(dsn, property.get().getName(), value);
249 updateStatus(ONLINE);
253 logger.debug("Does not know how to handle command `{}` ({}) on channel `{}`!", command,
254 command.getClass().getSimpleName(), channelUID);
257 private Optional<DeviceProperty.LongDeviceProperty> findLongProperty(String name, String shortName)
258 throws SalusApiException {
259 var deviceProperties = findDeviceProperties();
260 var property = deviceProperties.stream().filter(p -> p.getName().equals(name))
261 .filter(DeviceProperty.LongDeviceProperty.class::isInstance)
262 .map(DeviceProperty.LongDeviceProperty.class::cast).findAny();
263 if (property.isEmpty()) {
264 property = deviceProperties.stream().filter(p -> p.getName().contains(shortName))
265 .filter(DeviceProperty.LongDeviceProperty.class::isInstance)
266 .map(DeviceProperty.LongDeviceProperty.class::cast).findAny();
268 if (property.isEmpty()) {
269 logger.debug("{}/{} property not found!", name, shortName);
274 private SortedSet<DeviceProperty<?>> findDeviceProperties() throws SalusApiException {
275 return this.cloudApi.findPropertiesForDevice(dsn);