]> git.basschouten.com Git - openhab-addons.git/blob
d8fce1a05b8a2f27d275ce01145276bedffb3a60
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.senechome.internal;
14
15 import static org.openhab.core.types.RefreshType.REFRESH;
16
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.time.Duration;
21 import java.util.Date;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
26
27 import javax.measure.quantity.Dimensionless;
28 import javax.measure.quantity.Power;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.openhab.binding.senechome.internal.json.SenecHomeResponse;
34 import org.openhab.core.cache.ExpiringCache;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.library.unit.SmartHomeUnits;
39 import org.openhab.core.thing.Channel;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 /**
50  * The {@link SenecHomeHandler} is responsible for handling commands, which are
51  * sent to one of the channels.
52  *
53  * @author Steven Schwarznau - Initial contribution
54  */
55 @NonNullByDefault
56 public class SenecHomeHandler extends BaseThingHandler {
57
58     private final Logger logger = LoggerFactory.getLogger(SenecHomeHandler.class);
59     private final static String VALUE_TYPE_INT = "u3";
60     private final static String VALUE_TYPE_UNSIGNED_INT = "u8";
61     private final static String VALUE_TYPE_FLOAT = "fl";
62
63     private @Nullable ScheduledFuture<?> refreshJob;
64     private @Nullable PowerLimitationStatusDTO limitationStatus = null;
65     private final @Nullable SenecHomeApi senecHomeApi;
66     private SenecHomeConfigurationDTO config = new SenecHomeConfigurationDTO();
67     private final ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofSeconds(5), this::refreshState);
68
69     public SenecHomeHandler(Thing thing, HttpClient httpClient) {
70         super(thing);
71         this.senecHomeApi = new SenecHomeApi(httpClient);
72     }
73
74     @Override
75     public void handleRemoval() {
76         stopJobIfRunning();
77         super.handleRemoval();
78     }
79
80     @Override
81     public void handleCommand(ChannelUID channelUID, Command command) {
82         if (command == REFRESH) {
83             logger.debug("Refreshing {}", channelUID);
84             refresh();
85         } else {
86             logger.trace("The SenecHome-Binding is a read-only binding and can not handle commands");
87         }
88     }
89
90     @Override
91     public void dispose() {
92         stopJobIfRunning();
93         super.dispose();
94     }
95
96     protected void stopJobIfRunning() {
97         final ScheduledFuture<?> refreshJob = this.refreshJob;
98         if (refreshJob != null && !refreshJob.isCancelled()) {
99             refreshJob.cancel(true);
100             this.refreshJob = null;
101         }
102     }
103
104     @Override
105     public void initialize() {
106         config = getConfigAs(SenecHomeConfigurationDTO.class);
107         senecHomeApi.setHostname(config.hostname);
108         refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshInterval, TimeUnit.SECONDS);
109         limitationStatus = null;
110     }
111
112     private void refresh() {
113         refreshCache.getValue();
114     }
115
116     public @Nullable Boolean refreshState() {
117         try {
118             SenecHomeResponse response = senecHomeApi.getStatistics();
119             logger.trace("received {}", response);
120
121             BigDecimal pvLimitation = new BigDecimal(100).subtract(getSenecValue(response.limitation.powerLimitation))
122                     .setScale(0, RoundingMode.HALF_UP);
123
124             updateState(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_LIMITATION,
125                     new QuantityType<Dimensionless>(pvLimitation, SmartHomeUnits.PERCENT));
126
127             Channel channelLimitationState = getThing()
128                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_LIMITATION_STATE);
129             updatePowerLimitationStatus(channelLimitationState,
130                     (100 - pvLimitation.intValue()) <= config.limitationTresholdValue, config.limitationDuration);
131
132             Channel channelConsumption = getThing()
133                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_CONSUMPTION);
134             updateState(channelConsumption.getUID(),
135                     new QuantityType<Power>(
136                             getSenecValue(response.energy.homePowerConsumption).setScale(2, RoundingMode.HALF_UP),
137                             SmartHomeUnits.WATT));
138
139             Channel channelEnergyProduction = getThing()
140                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_ENERGY_PRODUCTION);
141             updateState(channelEnergyProduction.getUID(),
142                     new QuantityType<Power>(
143                             getSenecValue(response.energy.inverterPowerGeneration).setScale(0, RoundingMode.HALF_UP),
144                             SmartHomeUnits.WATT));
145
146             Channel channelBatteryPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_POWER);
147             updateState(channelBatteryPower.getUID(),
148                     new QuantityType<Power>(
149                             getSenecValue(response.energy.batteryPower).setScale(2, RoundingMode.HALF_UP),
150                             SmartHomeUnits.WATT));
151
152             Channel channelBatteryFuelCharge = getThing()
153                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_FUEL_CHARGE);
154             updateState(channelBatteryFuelCharge.getUID(),
155                     new QuantityType<Dimensionless>(
156                             getSenecValue(response.energy.batteryFuelCharge).setScale(0, RoundingMode.HALF_UP),
157                             SmartHomeUnits.PERCENT));
158
159             Channel channelBatteryState = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_STATE);
160             updateBatteryState(channelBatteryState, getSenecValue(response.energy.batteryState).intValue());
161
162             updateGridPowerValues(getSenecValue(response.grid.currentGridValue));
163
164             updateStatus(ThingStatus.ONLINE);
165         } catch (IOException | InterruptedException | TimeoutException | ExecutionException e) {
166             logger.warn("Error refreshing source '{}'", getThing().getUID(), e);
167             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
168                     "Could not connect to Senec web interface:" + e.getMessage());
169         }
170
171         return Boolean.TRUE;
172     }
173
174     protected BigDecimal getSenecValue(String value) {
175         String[] type = value.split("_");
176
177         if (VALUE_TYPE_INT.equalsIgnoreCase(type[0])) {
178             return new BigDecimal(Integer.valueOf(type[1], 16));
179         } else if (VALUE_TYPE_UNSIGNED_INT.equalsIgnoreCase(type[0])) {
180             return new BigDecimal(Integer.valueOf(type[1], 16));
181         } else if (VALUE_TYPE_FLOAT.equalsIgnoreCase(type[0])) {
182             return parseFloatValue(type[1]);
183         } else {
184             logger.warn("Unknown value type [{}]", type[0]);
185         }
186
187         return BigDecimal.ZERO;
188     }
189
190     /**
191      * Parse the hex coded float value of Senec device and return as BigDecimal
192      *
193      * @param value String with hex float
194      * @return BigDecimal with float value
195      */
196     private static BigDecimal parseFloatValue(String value) {
197         // sample: value = 43E26188
198
199         float f = Float.intBitsToFloat(Integer.parseUnsignedInt(value, 16));
200         return new BigDecimal(f);
201     }
202
203     protected void updatePowerLimitationStatus(Channel channel, boolean status, int duration) {
204         if (this.limitationStatus != null) {
205             if (this.limitationStatus.state == status) {
206                 long stateSince = new Date().getTime() - this.limitationStatus.time;
207
208                 if (((int) (stateSince / 1000)) < duration) {
209                     // skip updating state (possible flapping state)
210                     return;
211                 } else {
212                     logger.debug("{} longer than required duration {}", status, duration);
213                 }
214             } else {
215                 this.limitationStatus.state = status;
216                 this.limitationStatus.time = new Date().getTime();
217
218                 // skip updating state (state changed, possible flapping state)
219                 return;
220             }
221         } else {
222             this.limitationStatus = new PowerLimitationStatusDTO();
223             this.limitationStatus.state = status;
224         }
225
226         logger.debug("Updating power limitation state {}", status);
227         updateState(channel.getUID(), status ? OnOffType.ON : OnOffType.OFF);
228     }
229
230     protected void updateBatteryState(Channel channel, int code) {
231         updateState(channel.getUID(), new StringType(SenecBatteryStatus.descriptionFromCode(code)));
232     }
233
234     protected void updateGridPowerValues(BigDecimal gridTotalValue) {
235         BigDecimal gridTotal = gridTotalValue.setScale(2, RoundingMode.HALF_UP);
236
237         Channel channelGridPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER);
238         if (channelGridPower != null) {
239             updateState(channelGridPower.getUID(), new QuantityType<Power>(gridTotal, SmartHomeUnits.WATT));
240         }
241
242         Channel channelGridPowerSupply = getThing()
243                 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_SUPPLY);
244         if (channelGridPowerSupply != null) {
245             BigDecimal gridSupply = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? gridTotal.abs() : BigDecimal.ZERO;
246             updateState(channelGridPowerSupply.getUID(), new QuantityType<Power>(gridSupply, SmartHomeUnits.WATT));
247         }
248
249         Channel channelGridPowerDraw = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_DRAW);
250         if (channelGridPowerDraw != null) {
251             BigDecimal gridDraw = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : gridTotal.abs();
252             updateState(channelGridPowerDraw.getUID(), new QuantityType<Power>(gridDraw, SmartHomeUnits.WATT));
253         }
254     }
255 }