]> git.basschouten.com Git - openhab-addons.git/blob
e70a97abae5ca85ed269413116bbae9f5c2257ba
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.ElectricCurrent;
29 import javax.measure.quantity.ElectricPotential;
30 import javax.measure.quantity.Energy;
31 import javax.measure.quantity.Frequency;
32 import javax.measure.quantity.Power;
33
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.eclipse.jetty.client.HttpClient;
37 import org.openhab.binding.senechome.internal.json.SenecHomeResponse;
38 import org.openhab.core.cache.ExpiringCache;
39 import org.openhab.core.library.types.DecimalType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.QuantityType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.library.unit.Units;
44 import org.openhab.core.thing.Channel;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.types.Command;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import com.google.gson.JsonSyntaxException;
55
56 /**
57  * The {@link SenecHomeHandler} is responsible for handling commands, which are
58  * sent to one of the channels.
59  *
60  * @author Steven Schwarznau - Initial contribution
61  */
62 @NonNullByDefault
63 public class SenecHomeHandler extends BaseThingHandler {
64
65     private final Logger logger = LoggerFactory.getLogger(SenecHomeHandler.class);
66     private final static String VALUE_TYPE_INT = "u3";
67     private final static String VALUE_TYPE_UNSIGNED_INT = "u8";
68     private final static String VALUE_TYPE_FLOAT = "fl";
69
70     private @Nullable ScheduledFuture<?> refreshJob;
71     private @Nullable PowerLimitationStatusDTO limitationStatus = null;
72     private final @Nullable SenecHomeApi senecHomeApi;
73     private SenecHomeConfigurationDTO config = new SenecHomeConfigurationDTO();
74     private final ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofSeconds(5), this::refreshState);
75
76     public SenecHomeHandler(Thing thing, HttpClient httpClient) {
77         super(thing);
78         this.senecHomeApi = new SenecHomeApi(httpClient);
79     }
80
81     @Override
82     public void handleRemoval() {
83         stopJobIfRunning();
84         super.handleRemoval();
85     }
86
87     @Override
88     public void handleCommand(ChannelUID channelUID, Command command) {
89         if (command == REFRESH) {
90             logger.debug("Refreshing {}", channelUID);
91             refresh();
92         } else {
93             logger.trace("The SenecHome-Binding is a read-only binding and can not handle commands");
94         }
95     }
96
97     @Override
98     public void dispose() {
99         stopJobIfRunning();
100         super.dispose();
101     }
102
103     protected void stopJobIfRunning() {
104         final ScheduledFuture<?> refreshJob = this.refreshJob;
105         if (refreshJob != null && !refreshJob.isCancelled()) {
106             refreshJob.cancel(true);
107             this.refreshJob = null;
108         }
109     }
110
111     @Override
112     public void initialize() {
113         config = getConfigAs(SenecHomeConfigurationDTO.class);
114         senecHomeApi.setHostname(config.hostname);
115         refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshInterval, TimeUnit.SECONDS);
116         limitationStatus = null;
117     }
118
119     private void refresh() {
120         refreshCache.getValue();
121     }
122
123     public @Nullable Boolean refreshState() {
124         SenecHomeResponse response = null;
125         try {
126             response = senecHomeApi.getStatistics();
127             logger.trace("received {}", response);
128
129             BigDecimal pvLimitation = new BigDecimal(100).subtract(getSenecValue(response.limitation.powerLimitation))
130                     .setScale(0, RoundingMode.HALF_UP);
131
132             updateState(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_LIMITATION,
133                     new QuantityType<Dimensionless>(pvLimitation, Units.PERCENT));
134
135             Channel channelLimitationState = getThing()
136                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_LIMITATION_STATE);
137             if (channelLimitationState != null) {
138                 updatePowerLimitationStatus(channelLimitationState,
139                         (100 - pvLimitation.intValue()) <= config.limitationTresholdValue, config.limitationDuration);
140             }
141
142             Channel channelConsumption = getThing()
143                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_CONSUMPTION);
144             if (channelConsumption != null) {
145                 updateState(channelConsumption.getUID(),
146                         new QuantityType<Power>(
147                                 getSenecValue(response.energy.homePowerConsumption).setScale(2, RoundingMode.HALF_UP),
148                                 Units.WATT));
149             }
150
151             Channel channelEnergyProduction = getThing()
152                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_ENERGY_PRODUCTION);
153             if (channelEnergyProduction != null) {
154                 updateState(channelEnergyProduction.getUID(), new QuantityType<Power>(
155                         getSenecValue(response.energy.inverterPowerGeneration).setScale(0, RoundingMode.HALF_UP),
156                         Units.WATT));
157             }
158
159             Channel channelBatteryPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_POWER);
160             if (channelBatteryPower != null) {
161                 updateState(channelBatteryPower.getUID(), new QuantityType<Power>(
162                         getSenecValue(response.energy.batteryPower).setScale(2, RoundingMode.HALF_UP), Units.WATT));
163             }
164
165             Channel channelBatteryFuelCharge = getThing()
166                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_FUEL_CHARGE);
167             if (channelBatteryFuelCharge != null) {
168                 updateState(channelBatteryFuelCharge.getUID(),
169                         new QuantityType<Dimensionless>(
170                                 getSenecValue(response.energy.batteryFuelCharge).setScale(0, RoundingMode.HALF_UP),
171                                 Units.PERCENT));
172             }
173
174             Channel channelGridCurrentPhase1 = getThing()
175                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_CURRENT_PH1);
176             updateState(channelGridCurrentPhase1.getUID(), new QuantityType<ElectricCurrent>(
177                     getSenecValue(response.grid.currentGridCurrentPerPhase[0]).setScale(2, RoundingMode.HALF_UP),
178                     Units.AMPERE));
179
180             Channel channelGridCurrentPhase2 = getThing()
181                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_CURRENT_PH2);
182             updateState(channelGridCurrentPhase2.getUID(), new QuantityType<ElectricCurrent>(
183                     getSenecValue(response.grid.currentGridCurrentPerPhase[1]).setScale(2, RoundingMode.HALF_UP),
184                     Units.AMPERE));
185
186             Channel channelGridCurrentPhase3 = getThing()
187                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_CURRENT_PH3);
188             updateState(channelGridCurrentPhase3.getUID(), new QuantityType<ElectricCurrent>(
189                     getSenecValue(response.grid.currentGridCurrentPerPhase[2]).setScale(2, RoundingMode.HALF_UP),
190                     Units.AMPERE));
191
192             Channel channelGridPowerPhase1 = getThing()
193                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_PH1);
194             updateState(channelGridPowerPhase1.getUID(),
195                     new QuantityType<Power>(
196                             getSenecValue(response.grid.currentGridPowerPerPhase[0]).setScale(2, RoundingMode.HALF_UP),
197                             Units.WATT));
198
199             Channel channelGridPowerPhase2 = getThing()
200                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_PH2);
201             updateState(channelGridPowerPhase2.getUID(),
202                     new QuantityType<Power>(
203                             getSenecValue(response.grid.currentGridPowerPerPhase[1]).setScale(2, RoundingMode.HALF_UP),
204                             Units.WATT));
205
206             Channel channelGridPowerPhase3 = getThing()
207                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_PH3);
208             updateState(channelGridPowerPhase3.getUID(),
209                     new QuantityType<Power>(
210                             getSenecValue(response.grid.currentGridPowerPerPhase[2]).setScale(2, RoundingMode.HALF_UP),
211                             Units.WATT));
212
213             Channel channelGridVoltagePhase1 = getThing()
214                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_VOLTAGE_PH1);
215             updateState(channelGridVoltagePhase1.getUID(), new QuantityType<ElectricPotential>(
216                     getSenecValue(response.grid.currentGridVoltagePerPhase[0]).setScale(2, RoundingMode.HALF_UP),
217                     Units.VOLT));
218
219             Channel channelGridVoltagePhase2 = getThing()
220                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_VOLTAGE_PH2);
221             updateState(channelGridVoltagePhase2.getUID(), new QuantityType<ElectricPotential>(
222                     getSenecValue(response.grid.currentGridVoltagePerPhase[1]).setScale(2, RoundingMode.HALF_UP),
223                     Units.VOLT));
224
225             Channel channelGridVoltagePhase3 = getThing()
226                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_VOLTAGE_PH3);
227             updateState(channelGridVoltagePhase3.getUID(), new QuantityType<ElectricPotential>(
228                     getSenecValue(response.grid.currentGridVoltagePerPhase[2]).setScale(2, RoundingMode.HALF_UP),
229                     Units.VOLT));
230
231             Channel channelGridFrequency = getThing()
232                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_FREQUENCY);
233             updateState(channelGridFrequency.getUID(), new QuantityType<Frequency>(
234                     getSenecValue(response.grid.currentGridFrequency).setScale(2, RoundingMode.HALF_UP), Units.HERTZ));
235
236             Channel channelBatteryStateValue = getThing()
237                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_STATE_VALUE);
238             updateState(channelBatteryStateValue.getUID(),
239                     new DecimalType(getSenecValue(response.energy.batteryState).intValue()));
240
241             Channel channelLiveBatCharge = getThing()
242                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_LIVE_BAT_CHARGE);
243             updateState(channelLiveBatCharge.getUID(),
244                     new QuantityType<Energy>(
245                             getSenecValue(response.statistics.liveBatCharge).setScale(2, RoundingMode.HALF_UP),
246                             Units.WATT_HOUR));
247
248             Channel channelLiveBatDischarge = getThing()
249                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_LIVE_BAT_DISCHARGE);
250             updateState(channelLiveBatDischarge.getUID(),
251                     new QuantityType<Energy>(
252                             getSenecValue(response.statistics.liveBatDischarge).setScale(2, RoundingMode.HALF_UP),
253                             Units.WATT_HOUR));
254
255             Channel channelLiveGridImport = getThing()
256                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_LIVE_GRID_IMPORT);
257             updateState(channelLiveGridImport.getUID(),
258                     new QuantityType<Energy>(
259                             getSenecValue(response.statistics.liveGridImport).setScale(2, RoundingMode.HALF_UP),
260                             Units.WATT_HOUR));
261
262             Channel channelLiveGridExport = getThing()
263                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_LIVE_GRID_EXPORT);
264             updateState(channelLiveGridExport.getUID(),
265                     new QuantityType<Energy>(
266                             getSenecValue(response.statistics.liveGridExport).setScale(2, RoundingMode.HALF_UP),
267                             Units.WATT_HOUR));
268
269             Channel channelBatteryVoltage = getThing()
270                     .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_VOLTAGE);
271             updateState(channelBatteryVoltage.getUID(), new QuantityType<ElectricPotential>(
272                     getSenecValue(response.energy.batteryVoltage).setScale(2, RoundingMode.HALF_UP), Units.VOLT));
273
274             Channel channelBatteryState = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_STATE);
275             if (channelBatteryState != null) {
276                 updateBatteryState(channelBatteryState, getSenecValue(response.energy.batteryState).intValue());
277             }
278
279             updateGridPowerValues(getSenecValue(response.grid.currentGridValue));
280
281             updateStatus(ThingStatus.ONLINE);
282         } catch (JsonSyntaxException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
283             if (response == null) {
284                 logger.trace("Faulty response: is null");
285             } else {
286                 logger.trace("Faulty response: {}", response.toString());
287             }
288             logger.warn("Error refreshing source '{}'", getThing().getUID(), e);
289             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
290                     "Could not connect to Senec web interface:" + e.getMessage());
291         }
292
293         return Boolean.TRUE;
294     }
295
296     protected BigDecimal getSenecValue(String value) {
297         String[] type = value.split("_");
298
299         if (VALUE_TYPE_INT.equalsIgnoreCase(type[0])) {
300             return new BigDecimal(Integer.valueOf(type[1], 16));
301         } else if (VALUE_TYPE_UNSIGNED_INT.equalsIgnoreCase(type[0])) {
302             return new BigDecimal(Integer.valueOf(type[1], 16));
303         } else if (VALUE_TYPE_FLOAT.equalsIgnoreCase(type[0])) {
304             return parseFloatValue(type[1]);
305         } else {
306             logger.warn("Unknown value type [{}]", type[0]);
307         }
308
309         return BigDecimal.ZERO;
310     }
311
312     /**
313      * Parse the hex coded float value of Senec device and return as BigDecimal
314      *
315      * @param value String with hex float
316      * @return BigDecimal with float value
317      */
318     private static BigDecimal parseFloatValue(String value) {
319         // sample: value = 43E26188
320
321         float f = Float.intBitsToFloat(Integer.parseUnsignedInt(value, 16));
322         return new BigDecimal(f);
323     }
324
325     protected void updatePowerLimitationStatus(Channel channel, boolean status, int duration) {
326         if (this.limitationStatus != null) {
327             if (this.limitationStatus.state == status) {
328                 long stateSince = new Date().getTime() - this.limitationStatus.time;
329
330                 if (((int) (stateSince / 1000)) < duration) {
331                     // skip updating state (possible flapping state)
332                     return;
333                 } else {
334                     logger.debug("{} longer than required duration {}", status, duration);
335                 }
336             } else {
337                 this.limitationStatus.state = status;
338                 this.limitationStatus.time = new Date().getTime();
339
340                 // skip updating state (state changed, possible flapping state)
341                 return;
342             }
343         } else {
344             this.limitationStatus = new PowerLimitationStatusDTO();
345             this.limitationStatus.state = status;
346         }
347
348         logger.debug("Updating power limitation state {}", status);
349         updateState(channel.getUID(), status ? OnOffType.ON : OnOffType.OFF);
350     }
351
352     protected void updateBatteryState(Channel channel, int code) {
353         updateState(channel.getUID(), new StringType(SenecBatteryStatus.descriptionFromCode(code)));
354     }
355
356     protected void updateGridPowerValues(BigDecimal gridTotalValue) {
357         BigDecimal gridTotal = gridTotalValue.setScale(2, RoundingMode.HALF_UP);
358
359         Channel channelGridPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER);
360         if (channelGridPower != null) {
361             updateState(channelGridPower.getUID(), new QuantityType<Power>(gridTotal, Units.WATT));
362         }
363
364         Channel channelGridPowerSupply = getThing()
365                 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_SUPPLY);
366         if (channelGridPowerSupply != null) {
367             BigDecimal gridSupply = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? gridTotal.abs() : BigDecimal.ZERO;
368             updateState(channelGridPowerSupply.getUID(), new QuantityType<Power>(gridSupply, Units.WATT));
369         }
370
371         Channel channelGridPowerDraw = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_DRAW);
372         if (channelGridPowerDraw != null) {
373             BigDecimal gridDraw = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : gridTotal.abs();
374             updateState(channelGridPowerDraw.getUID(), new QuantityType<Power>(gridDraw, Units.WATT));
375         }
376     }
377 }