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.groheondus.internal.handler;
15 import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_CONFIG_TIMEFRAME;
16 import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_NAME;
17 import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_PRESSURE;
18 import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_TEMPERATURE_GUARD;
19 import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_VALVE_OPEN;
20 import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_WATERCONSUMPTION;
21 import static org.openhab.binding.groheondus.internal.GroheOndusBindingConstants.CHANNEL_WATERCONSUMPTION_SINCE_MIDNIGHT;
23 import java.io.IOException;
24 import java.math.BigDecimal;
25 import java.time.Instant;
26 import java.time.ZoneId;
27 import java.time.ZonedDateTime;
28 import java.time.temporal.ChronoUnit;
29 import java.util.Collections;
30 import java.util.Comparator;
31 import java.util.List;
32 import java.util.Optional;
34 import javax.measure.quantity.Volume;
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.library.unit.SIUnits;
42 import org.openhab.core.library.unit.Units;
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.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.State;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
55 import io.github.floriansw.ondus.api.OndusService;
56 import io.github.floriansw.ondus.api.model.BaseApplianceCommand;
57 import io.github.floriansw.ondus.api.model.BaseApplianceData;
58 import io.github.floriansw.ondus.api.model.guard.Appliance;
59 import io.github.floriansw.ondus.api.model.guard.ApplianceCommand;
60 import io.github.floriansw.ondus.api.model.guard.ApplianceData;
61 import io.github.floriansw.ondus.api.model.guard.ApplianceData.Data;
62 import io.github.floriansw.ondus.api.model.guard.ApplianceData.Measurement;
65 * @author Florian Schmidt and Arne Wohlert - Initial contribution
68 public class GroheOndusSenseGuardHandler<T, M> extends GroheOndusBaseHandler<Appliance, Data> {
69 private static final int MIN_API_TIMEFRAME_DAYS = 1;
70 private static final int MAX_API_TIMEFRAME_DAYS = 90;
71 private static final int DEFAULT_TIMEFRAME_DAYS = 1;
73 private final Logger logger = LoggerFactory.getLogger(GroheOndusSenseGuardHandler.class);
75 public GroheOndusSenseGuardHandler(Thing thing, int thingCounter) {
76 super(thing, Appliance.TYPE, thingCounter);
80 protected int getPollingInterval(Appliance appliance) {
81 if (config.pollingInterval > 0) {
82 return config.pollingInterval;
84 return appliance.getConfig().getMeasurementTransmissionIntervall();
88 protected void updateChannel(ChannelUID channelUID, Appliance appliance, Data dataPoint) {
89 String channelId = channelUID.getIdWithoutGroup();
90 State newState = UnDefType.UNDEF;
91 Measurement lastMeasurement = getLastMeasurement(dataPoint);
94 newState = new StringType(appliance.getName());
96 case CHANNEL_PRESSURE:
97 newState = new QuantityType<>(lastMeasurement.getPressure(), Units.BAR);
99 case CHANNEL_TEMPERATURE_GUARD:
100 newState = new QuantityType<>(lastMeasurement.getTemperatureGuard(), SIUnits.CELSIUS);
102 case CHANNEL_VALVE_OPEN:
103 OnOffType valveOpenType = getValveOpenType(appliance);
104 if (valveOpenType != null) {
105 newState = valveOpenType;
108 case CHANNEL_WATERCONSUMPTION:
109 newState = sumWaterConsumption(dataPoint);
111 case CHANNEL_WATERCONSUMPTION_SINCE_MIDNIGHT:
112 newState = sumWaterConsumptionSinceMidnight(dataPoint);
115 throw new IllegalArgumentException("Channel " + channelUID + " not supported.");
117 updateState(channelUID, newState);
120 private QuantityType<Volume> sumWaterConsumptionSinceMidnight(Data dataPoint) {
121 ZonedDateTime earliestWithdrawal = ZonedDateTime.now(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS);
122 ZonedDateTime latestWithdrawal = earliestWithdrawal.plus(1, ChronoUnit.DAYS);
124 Double waterConsumption = dataPoint.getWithdrawals().stream()
125 .filter(e -> earliestWithdrawal.isBefore(e.starttime.toInstant().atZone(ZoneId.systemDefault()))
126 && latestWithdrawal.isAfter(e.starttime.toInstant().atZone(ZoneId.systemDefault())))
127 .mapToDouble(withdrawal -> withdrawal.getWaterconsumption()).sum();
128 return new QuantityType<>(waterConsumption, Units.LITRE);
131 private QuantityType<Volume> sumWaterConsumption(Data dataPoint) {
132 Double waterConsumption = dataPoint.getWithdrawals().stream()
133 .mapToDouble(withdrawal -> withdrawal.getWaterconsumption()).sum();
134 return new QuantityType<Volume>(waterConsumption, Units.LITRE);
137 private Measurement getLastMeasurement(Data dataPoint) {
138 List<Measurement> measurementList = dataPoint.getMeasurement();
139 return measurementList.isEmpty() ? new Measurement() : measurementList.get(measurementList.size() - 1);
143 private OnOffType getValveOpenType(Appliance appliance) {
144 OndusService service = getOndusService();
145 if (service == null) {
148 Optional<BaseApplianceCommand> commandOptional;
150 commandOptional = service.applianceCommand(appliance);
151 } catch (IOException e) {
152 logger.debug("Could not get appliance command", e);
155 if (commandOptional.isEmpty()) {
158 if (commandOptional.get().getType() != Appliance.TYPE) {
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.notsenseguard");
162 return OnOffType.from(((ApplianceCommand) commandOptional.get()).getCommand().getValveOpen());
166 protected Data getLastDataPoint(Appliance appliance) {
167 if (getOndusService() == null) {
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "@text/error.noservice");
172 ApplianceData applianceData = getApplianceData(appliance);
173 if (applianceData == null) {
174 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.empty.response");
177 Data data = applianceData.getData();
178 Collections.sort(data.measurement, Comparator.comparing(e -> ZonedDateTime.parse(e.timestamp)));
179 Collections.sort(data.withdrawals, Comparator.comparing(e -> e.starttime));
183 private @Nullable ApplianceData getApplianceData(Appliance appliance) {
184 Instant from = fromTime();
185 // Truncated to date only inside api package
186 Instant to = Instant.now().plus(1, ChronoUnit.DAYS);
188 OndusService service = getOndusService();
189 if (service == null) {
193 logger.debug("Fetching data for {} from {} to {}", thing.getUID(), from, to);
194 BaseApplianceData applianceData = service.applianceData(appliance, from, to).orElse(null);
195 if (applianceData != null) {
196 if (applianceData.getType() != Appliance.TYPE) {
197 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
198 "@text/error.notsenseguard");
201 return (ApplianceData) applianceData;
203 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
204 "@text/error.failedtoloaddata");
206 } catch (IOException e) {
207 logger.debug("Could not load appliance data for {}", thing.getUID(), e);
212 private Instant fromTime() {
213 Instant from = Instant.now().minus(DEFAULT_TIMEFRAME_DAYS, ChronoUnit.DAYS);
214 Channel waterconsumptionChannel = this.thing.getChannel(CHANNEL_WATERCONSUMPTION);
215 if (waterconsumptionChannel == null) {
219 Object timeframeConfig = waterconsumptionChannel.getConfiguration().get(CHANNEL_CONFIG_TIMEFRAME);
220 if (!(timeframeConfig instanceof BigDecimal)) {
224 int timeframe = ((BigDecimal) timeframeConfig).intValue();
225 if (timeframe < MIN_API_TIMEFRAME_DAYS && timeframe > MAX_API_TIMEFRAME_DAYS) {
227 "timeframe configuration of waterconsumption channel needs to be a number between 1 to 90, got {}",
232 return Instant.now().minus(timeframe, ChronoUnit.DAYS);
236 public void handleCommand(ChannelUID channelUID, Command command) {
237 if (command instanceof RefreshType) {
242 if (!CHANNEL_VALVE_OPEN.equals(channelUID.getIdWithoutGroup())) {
245 if (!(command instanceof OnOffType)) {
246 logger.debug("Invalid command received for channel. Expected OnOffType, received {}.",
247 command.getClass().getName());
250 OnOffType openClosedCommand = (OnOffType) command;
251 boolean openState = openClosedCommand == OnOffType.ON;
253 OndusService service = getOndusService();
254 if (service == null) {
257 Appliance appliance = getAppliance(service);
258 if (appliance == null) {
262 service.setValveOpen(appliance, openState);
264 } catch (IOException e) {
265 logger.debug("Could not update valve open state", e);