2 * Copyright (c) 2010-2020 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.senechome.internal;
15 import static org.openhab.core.types.RefreshType.REFRESH;
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;
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;
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.SmartHomeUnits;
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;
55 * The {@link SenecHomeHandler} is responsible for handling commands, which are
56 * sent to one of the channels.
58 * @author Steven Schwarznau - Initial contribution
61 public class SenecHomeHandler extends BaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(SenecHomeHandler.class);
64 private final static String VALUE_TYPE_INT = "u3";
65 private final static String VALUE_TYPE_UNSIGNED_INT = "u8";
66 private final static String VALUE_TYPE_FLOAT = "fl";
68 private @Nullable ScheduledFuture<?> refreshJob;
69 private @Nullable PowerLimitationStatusDTO limitationStatus = null;
70 private final @Nullable SenecHomeApi senecHomeApi;
71 private SenecHomeConfigurationDTO config = new SenecHomeConfigurationDTO();
72 private final ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofSeconds(5), this::refreshState);
74 public SenecHomeHandler(Thing thing, HttpClient httpClient) {
76 this.senecHomeApi = new SenecHomeApi(httpClient);
80 public void handleRemoval() {
82 super.handleRemoval();
86 public void handleCommand(ChannelUID channelUID, Command command) {
87 if (command == REFRESH) {
88 logger.debug("Refreshing {}", channelUID);
91 logger.trace("The SenecHome-Binding is a read-only binding and can not handle commands");
96 public void dispose() {
101 protected void stopJobIfRunning() {
102 final ScheduledFuture<?> refreshJob = this.refreshJob;
103 if (refreshJob != null && !refreshJob.isCancelled()) {
104 refreshJob.cancel(true);
105 this.refreshJob = null;
110 public void initialize() {
111 config = getConfigAs(SenecHomeConfigurationDTO.class);
112 senecHomeApi.setHostname(config.hostname);
113 refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshInterval, TimeUnit.SECONDS);
114 limitationStatus = null;
117 private void refresh() {
118 refreshCache.getValue();
121 public @Nullable Boolean refreshState() {
123 SenecHomeResponse response = senecHomeApi.getStatistics();
124 logger.trace("received {}", response);
126 BigDecimal pvLimitation = new BigDecimal(100).subtract(getSenecValue(response.limitation.powerLimitation))
127 .setScale(0, RoundingMode.HALF_UP);
129 updateState(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_LIMITATION,
130 new QuantityType<Dimensionless>(pvLimitation, SmartHomeUnits.PERCENT));
132 Channel channelLimitationState = getThing()
133 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_LIMITATION_STATE);
134 if (channelLimitationState != null) {
135 updatePowerLimitationStatus(channelLimitationState,
136 (100 - pvLimitation.intValue()) <= config.limitationTresholdValue, config.limitationDuration);
139 Channel channelConsumption = getThing()
140 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_CONSUMPTION);
141 if (channelConsumption != null) {
142 updateState(channelConsumption.getUID(),
143 new QuantityType<Power>(
144 getSenecValue(response.energy.homePowerConsumption).setScale(2, RoundingMode.HALF_UP),
145 SmartHomeUnits.WATT));
148 Channel channelEnergyProduction = getThing()
149 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_ENERGY_PRODUCTION);
150 if (channelEnergyProduction != null) {
151 updateState(channelEnergyProduction.getUID(), new QuantityType<Power>(
152 getSenecValue(response.energy.inverterPowerGeneration).setScale(0, RoundingMode.HALF_UP),
153 SmartHomeUnits.WATT));
156 Channel channelBatteryPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_POWER);
157 if (channelBatteryPower != null) {
158 updateState(channelBatteryPower.getUID(),
159 new QuantityType<Power>(
160 getSenecValue(response.energy.batteryPower).setScale(2, RoundingMode.HALF_UP),
161 SmartHomeUnits.WATT));
164 Channel channelBatteryFuelCharge = getThing()
165 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_FUEL_CHARGE);
166 if (channelBatteryFuelCharge != null) {
167 updateState(channelBatteryFuelCharge.getUID(),
168 new QuantityType<Dimensionless>(
169 getSenecValue(response.energy.batteryFuelCharge).setScale(0, RoundingMode.HALF_UP),
170 SmartHomeUnits.PERCENT));
173 Channel channelGridCurrentPhase1 = getThing()
174 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_CURRENT_PH1);
175 updateState(channelGridCurrentPhase1.getUID(), new QuantityType<ElectricCurrent>(
176 getSenecValue(response.grid.currentGridCurrentPerPhase[0]).setScale(2, RoundingMode.HALF_UP),
177 SmartHomeUnits.AMPERE));
179 Channel channelGridCurrentPhase2 = getThing()
180 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_CURRENT_PH2);
181 updateState(channelGridCurrentPhase2.getUID(), new QuantityType<ElectricCurrent>(
182 getSenecValue(response.grid.currentGridCurrentPerPhase[1]).setScale(2, RoundingMode.HALF_UP),
183 SmartHomeUnits.AMPERE));
185 Channel channelGridCurrentPhase3 = getThing()
186 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_CURRENT_PH3);
187 updateState(channelGridCurrentPhase3.getUID(), new QuantityType<ElectricCurrent>(
188 getSenecValue(response.grid.currentGridCurrentPerPhase[2]).setScale(2, RoundingMode.HALF_UP),
189 SmartHomeUnits.AMPERE));
191 Channel channelGridPowerPhase1 = getThing()
192 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_PH1);
193 updateState(channelGridPowerPhase1.getUID(),
194 new QuantityType<Power>(
195 getSenecValue(response.grid.currentGridPowerPerPhase[0]).setScale(2, RoundingMode.HALF_UP),
196 SmartHomeUnits.WATT));
198 Channel channelGridPowerPhase2 = getThing()
199 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_PH2);
200 updateState(channelGridPowerPhase2.getUID(),
201 new QuantityType<Power>(
202 getSenecValue(response.grid.currentGridPowerPerPhase[1]).setScale(2, RoundingMode.HALF_UP),
203 SmartHomeUnits.WATT));
205 Channel channelGridPowerPhase3 = getThing()
206 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_PH3);
207 updateState(channelGridPowerPhase3.getUID(),
208 new QuantityType<Power>(
209 getSenecValue(response.grid.currentGridPowerPerPhase[2]).setScale(2, RoundingMode.HALF_UP),
210 SmartHomeUnits.WATT));
212 Channel channelGridVoltagePhase1 = getThing()
213 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_VOLTAGE_PH1);
214 updateState(channelGridVoltagePhase1.getUID(), new QuantityType<ElectricPotential>(
215 getSenecValue(response.grid.currentGridVoltagePerPhase[0]).setScale(2, RoundingMode.HALF_UP),
216 SmartHomeUnits.VOLT));
218 Channel channelGridVoltagePhase2 = getThing()
219 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_VOLTAGE_PH2);
220 updateState(channelGridVoltagePhase2.getUID(), new QuantityType<ElectricPotential>(
221 getSenecValue(response.grid.currentGridVoltagePerPhase[1]).setScale(2, RoundingMode.HALF_UP),
222 SmartHomeUnits.VOLT));
224 Channel channelGridVoltagePhase3 = getThing()
225 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_VOLTAGE_PH3);
226 updateState(channelGridVoltagePhase3.getUID(), new QuantityType<ElectricPotential>(
227 getSenecValue(response.grid.currentGridVoltagePerPhase[2]).setScale(2, RoundingMode.HALF_UP),
228 SmartHomeUnits.VOLT));
230 Channel channelGridFrequency = getThing()
231 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_FREQUENCY);
232 updateState(channelGridFrequency.getUID(),
233 new QuantityType<Frequency>(
234 getSenecValue(response.grid.currentGridFrequency).setScale(2, RoundingMode.HALF_UP),
235 SmartHomeUnits.HERTZ));
237 Channel channelBatteryStateValue = getThing()
238 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_STATE_VALUE);
239 updateState(channelBatteryStateValue.getUID(),
240 new DecimalType(getSenecValue(response.energy.batteryState).intValue()));
242 Channel channelLiveBatCharge = getThing()
243 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_LIVE_BAT_CHARGE);
244 updateState(channelLiveBatCharge.getUID(),
245 new QuantityType<Energy>(
246 getSenecValue(response.statistics.liveBatCharge).setScale(2, RoundingMode.HALF_UP),
247 SmartHomeUnits.WATT_HOUR));
249 Channel channelLiveBatDischarge = getThing()
250 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_LIVE_BAT_DISCHARGE);
251 updateState(channelLiveBatDischarge.getUID(),
252 new QuantityType<Energy>(
253 getSenecValue(response.statistics.liveBatDischarge).setScale(2, RoundingMode.HALF_UP),
254 SmartHomeUnits.WATT_HOUR));
256 Channel channelLiveGridImport = getThing()
257 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_LIVE_GRID_IMPORT);
258 updateState(channelLiveGridImport.getUID(),
259 new QuantityType<Energy>(
260 getSenecValue(response.statistics.liveGridImport).setScale(2, RoundingMode.HALF_UP),
261 SmartHomeUnits.WATT_HOUR));
263 Channel channelLiveGridExport = getThing()
264 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_LIVE_GRID_EXPORT);
265 updateState(channelLiveGridExport.getUID(),
266 new QuantityType<Energy>(
267 getSenecValue(response.statistics.liveGridExport).setScale(2, RoundingMode.HALF_UP),
268 SmartHomeUnits.WATT_HOUR));
270 Channel channelBatteryVoltage = getThing()
271 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_VOLTAGE);
272 updateState(channelBatteryVoltage.getUID(),
273 new QuantityType<ElectricPotential>(
274 getSenecValue(response.energy.batteryVoltage).setScale(2, RoundingMode.HALF_UP),
275 SmartHomeUnits.VOLT));
277 Channel channelBatteryState = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_STATE);
278 if (channelBatteryState != null) {
279 updateBatteryState(channelBatteryState, getSenecValue(response.energy.batteryState).intValue());
282 updateGridPowerValues(getSenecValue(response.grid.currentGridValue));
284 updateStatus(ThingStatus.ONLINE);
285 } catch (IOException | InterruptedException | TimeoutException | ExecutionException e) {
286 logger.warn("Error refreshing source '{}'", getThing().getUID(), e);
287 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
288 "Could not connect to Senec web interface:" + e.getMessage());
294 protected BigDecimal getSenecValue(String value) {
295 String[] type = value.split("_");
297 if (VALUE_TYPE_INT.equalsIgnoreCase(type[0])) {
298 return new BigDecimal(Integer.valueOf(type[1], 16));
299 } else if (VALUE_TYPE_UNSIGNED_INT.equalsIgnoreCase(type[0])) {
300 return new BigDecimal(Integer.valueOf(type[1], 16));
301 } else if (VALUE_TYPE_FLOAT.equalsIgnoreCase(type[0])) {
302 return parseFloatValue(type[1]);
304 logger.warn("Unknown value type [{}]", type[0]);
307 return BigDecimal.ZERO;
311 * Parse the hex coded float value of Senec device and return as BigDecimal
313 * @param value String with hex float
314 * @return BigDecimal with float value
316 private static BigDecimal parseFloatValue(String value) {
317 // sample: value = 43E26188
319 float f = Float.intBitsToFloat(Integer.parseUnsignedInt(value, 16));
320 return new BigDecimal(f);
323 protected void updatePowerLimitationStatus(Channel channel, boolean status, int duration) {
324 if (this.limitationStatus != null) {
325 if (this.limitationStatus.state == status) {
326 long stateSince = new Date().getTime() - this.limitationStatus.time;
328 if (((int) (stateSince / 1000)) < duration) {
329 // skip updating state (possible flapping state)
332 logger.debug("{} longer than required duration {}", status, duration);
335 this.limitationStatus.state = status;
336 this.limitationStatus.time = new Date().getTime();
338 // skip updating state (state changed, possible flapping state)
342 this.limitationStatus = new PowerLimitationStatusDTO();
343 this.limitationStatus.state = status;
346 logger.debug("Updating power limitation state {}", status);
347 updateState(channel.getUID(), status ? OnOffType.ON : OnOffType.OFF);
350 protected void updateBatteryState(Channel channel, int code) {
351 updateState(channel.getUID(), new StringType(SenecBatteryStatus.descriptionFromCode(code)));
354 protected void updateGridPowerValues(BigDecimal gridTotalValue) {
355 BigDecimal gridTotal = gridTotalValue.setScale(2, RoundingMode.HALF_UP);
357 Channel channelGridPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER);
358 if (channelGridPower != null) {
359 updateState(channelGridPower.getUID(), new QuantityType<Power>(gridTotal, SmartHomeUnits.WATT));
362 Channel channelGridPowerSupply = getThing()
363 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_SUPPLY);
364 if (channelGridPowerSupply != null) {
365 BigDecimal gridSupply = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? gridTotal.abs() : BigDecimal.ZERO;
366 updateState(channelGridPowerSupply.getUID(), new QuantityType<Power>(gridSupply, SmartHomeUnits.WATT));
369 Channel channelGridPowerDraw = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_DRAW);
370 if (channelGridPowerDraw != null) {
371 BigDecimal gridDraw = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : gridTotal.abs();
372 updateState(channelGridPowerDraw.getUID(), new QuantityType<Power>(gridDraw, SmartHomeUnits.WATT));