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.Power;
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;
50 * The {@link SenecHomeHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
53 * @author Steven Schwarznau - Initial contribution
56 public class SenecHomeHandler extends BaseThingHandler {
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";
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);
69 public SenecHomeHandler(Thing thing, HttpClient httpClient) {
71 this.senecHomeApi = new SenecHomeApi(httpClient);
75 public void handleRemoval() {
77 super.handleRemoval();
81 public void handleCommand(ChannelUID channelUID, Command command) {
82 if (command == REFRESH) {
83 logger.debug("Refreshing {}", channelUID);
86 logger.trace("The SenecHome-Binding is a read-only binding and can not handle commands");
91 public void dispose() {
96 protected void stopJobIfRunning() {
97 final ScheduledFuture<?> refreshJob = this.refreshJob;
98 if (refreshJob != null && !refreshJob.isCancelled()) {
99 refreshJob.cancel(true);
100 this.refreshJob = null;
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;
112 private void refresh() {
113 refreshCache.getValue();
116 public @Nullable Boolean refreshState() {
118 SenecHomeResponse response = senecHomeApi.getStatistics();
119 logger.trace("received {}", response);
121 BigDecimal pvLimitation = new BigDecimal(100).subtract(getSenecValue(response.limitation.powerLimitation))
122 .setScale(0, RoundingMode.HALF_UP);
124 updateState(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_LIMITATION,
125 new QuantityType<Dimensionless>(pvLimitation, SmartHomeUnits.PERCENT));
127 Channel channelLimitationState = getThing()
128 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_POWER_LIMITATION_STATE);
129 updatePowerLimitationStatus(channelLimitationState,
130 (100 - pvLimitation.intValue()) <= config.limitationTresholdValue, config.limitationDuration);
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));
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));
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));
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));
159 Channel channelBatteryState = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_BATTERY_STATE);
160 updateBatteryState(channelBatteryState, getSenecValue(response.energy.batteryState).intValue());
162 updateGridPowerValues(getSenecValue(response.grid.currentGridValue));
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());
174 protected BigDecimal getSenecValue(String value) {
175 String[] type = value.split("_");
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]);
184 logger.warn("Unknown value type [{}]", type[0]);
187 return BigDecimal.ZERO;
191 * Parse the hex coded float value of Senec device and return as BigDecimal
193 * @param value String with hex float
194 * @return BigDecimal with float value
196 private static BigDecimal parseFloatValue(String value) {
197 // sample: value = 43E26188
199 float f = Float.intBitsToFloat(Integer.parseUnsignedInt(value, 16));
200 return new BigDecimal(f);
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;
208 if (((int) (stateSince / 1000)) < duration) {
209 // skip updating state (possible flapping state)
212 logger.debug("{} longer than required duration {}", status, duration);
215 this.limitationStatus.state = status;
216 this.limitationStatus.time = new Date().getTime();
218 // skip updating state (state changed, possible flapping state)
222 this.limitationStatus = new PowerLimitationStatusDTO();
223 this.limitationStatus.state = status;
226 logger.debug("Updating power limitation state {}", status);
227 updateState(channel.getUID(), status ? OnOffType.ON : OnOffType.OFF);
230 protected void updateBatteryState(Channel channel, int code) {
231 updateState(channel.getUID(), new StringType(SenecBatteryStatus.descriptionFromCode(code)));
234 protected void updateGridPowerValues(BigDecimal gridTotalValue) {
235 BigDecimal gridTotal = gridTotalValue.setScale(2, RoundingMode.HALF_UP);
237 Channel channelGridPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER);
238 if (channelGridPower != null) {
239 updateState(channelGridPower.getUID(), new QuantityType<Power>(gridTotal, SmartHomeUnits.WATT));
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));
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));