2 * Copyright (c) 2010-2023 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.binding.senechome.internal.SenecHomeBindingConstants.*;
16 import static org.openhab.core.types.RefreshType.REFRESH;
18 import java.io.IOException;
19 import java.math.BigDecimal;
20 import java.math.RoundingMode;
21 import java.time.Duration;
22 import java.util.Date;
23 import java.util.concurrent.ExecutionException;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27 import java.util.function.Function;
29 import javax.measure.Quantity;
30 import javax.measure.Unit;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.openhab.binding.senechome.internal.json.SenecHomeResponse;
36 import org.openhab.core.cache.ExpiringCache;
37 import org.openhab.core.library.types.DecimalType;
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.thing.binding.BaseThingHandler;
49 import org.openhab.core.types.Command;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
53 import com.google.gson.JsonParseException;
56 * The {@link SenecHomeHandler} is responsible for handling commands, which are
57 * sent to one of the channels.
59 * @author Steven Schwarznau - Initial contribution
60 * @author Erwin Guib - added more channels, added some convenience methods to reduce code duplication
63 public class SenecHomeHandler extends BaseThingHandler {
64 private final Logger logger = LoggerFactory.getLogger(SenecHomeHandler.class);
66 // divisor to transform from milli to kilo UNIT (e.g. mW => kW)
67 private static final BigDecimal DIVISOR_MILLI_TO_KILO = BigDecimal.valueOf(1000000);
68 // divisor to transform from milli to "iso" UNIT (e.g. mV => V)
69 private static final BigDecimal DIVISOR_MILLI_TO_ISO = BigDecimal.valueOf(1000);
70 // divisor to transform from "iso" to kilo UNIT (e.g. W => kW)
71 private static final BigDecimal DIVISOR_ISO_TO_KILO = BigDecimal.valueOf(1000);
72 // ix (x=1,3,8) types => hex encoded integer value
73 private static final String VALUE_TYPE_INT1 = "i1";
74 public static final String VALUE_TYPE_INT3 = "i3";
75 public static final String VALUE_TYPE_INT8 = "i8";
76 // ux (x=1,3,6,8) types => hex encoded unsigned value
77 private static final String VALUE_TYPE_DECIMAL = "u";
78 // fl => hex encoded float
79 private static final String VALUE_TYPE_FLOAT = "fl";
81 // public static final String VALUE_TYPE_STRING = "st";
83 private @Nullable ScheduledFuture<?> refreshJob;
84 private @Nullable PowerLimitationStatusDTO limitationStatus = null;
85 private final @Nullable SenecHomeApi senecHomeApi;
86 private SenecHomeConfigurationDTO config = new SenecHomeConfigurationDTO();
87 private final ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofSeconds(5), this::refreshState);
89 public SenecHomeHandler(Thing thing, HttpClient httpClient) {
91 this.senecHomeApi = new SenecHomeApi(httpClient);
95 public void handleRemoval() {
97 super.handleRemoval();
101 public void handleCommand(ChannelUID channelUID, Command command) {
102 if (command == REFRESH) {
103 logger.debug("Refreshing {}", channelUID);
106 logger.trace("The SenecHome-Binding is a read-only binding and can not handle commands");
111 public void dispose() {
116 protected void stopJobIfRunning() {
117 final ScheduledFuture<?> refreshJob = this.refreshJob;
118 if (refreshJob != null && !refreshJob.isCancelled()) {
119 refreshJob.cancel(true);
120 this.refreshJob = null;
125 public void initialize() {
126 config = getConfigAs(SenecHomeConfigurationDTO.class);
127 senecHomeApi.setHostname(config.hostname);
128 refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshInterval, TimeUnit.SECONDS);
129 limitationStatus = null;
132 private void refresh() {
133 refreshCache.getValue();
136 public @Nullable Boolean refreshState() {
137 SenecHomeResponse response = null;
139 response = senecHomeApi.getStatistics();
140 logger.trace("received {}", response);
142 BigDecimal pvLimitation = new BigDecimal(100).subtract(getSenecValue(response.power.powerLimitation))
143 .setScale(0, RoundingMode.HALF_UP);
144 updateState(CHANNEL_SENEC_POWER_LIMITATION, new QuantityType<>(pvLimitation, Units.PERCENT));
146 Channel channelLimitationState = getThing().getChannel(CHANNEL_SENEC_POWER_LIMITATION_STATE);
147 if (channelLimitationState != null) {
148 updatePowerLimitationStatus(channelLimitationState,
149 (100 - pvLimitation.intValue()) <= config.limitationTresholdValue, config.limitationDuration);
151 if (response.power.currentPerMpp != null) {
152 updateQtyState(CHANNEL_SENEC_CURRENT_MPP1, response.power.currentPerMpp[0], 2, Units.AMPERE);
153 updateQtyState(CHANNEL_SENEC_CURRENT_MPP2, response.power.currentPerMpp[1], 2, Units.AMPERE);
154 if (response.power.currentPerMpp.length > 2) {
156 updateQtyState(CHANNEL_SENEC_CURRENT_MPP3, response.power.currentPerMpp[2], 2, Units.AMPERE);
159 if (response.power.powerPerMpp != null) {
160 updateQtyState(CHANNEL_SENEC_POWER_MPP1, response.power.powerPerMpp[0], 2, Units.WATT);
161 updateQtyState(CHANNEL_SENEC_POWER_MPP2, response.power.powerPerMpp[1], 2, Units.WATT);
162 if (response.power.powerPerMpp.length > 2) {
163 updateQtyState(CHANNEL_SENEC_POWER_MPP3, response.power.powerPerMpp[2], 2, Units.WATT);
166 if (response.power.voltagePerMpp != null) {
167 updateQtyState(CHANNEL_SENEC_VOLTAGE_MPP1, response.power.voltagePerMpp[0], 2, Units.VOLT);
168 updateQtyState(CHANNEL_SENEC_VOLTAGE_MPP2, response.power.voltagePerMpp[1], 2, Units.VOLT);
169 if (response.power.voltagePerMpp.length > 2) {
170 updateQtyState(CHANNEL_SENEC_VOLTAGE_MPP3, response.power.voltagePerMpp[2], 2, Units.VOLT);
174 updateQtyState(CHANNEL_SENEC_POWER_CONSUMPTION, response.energy.housePowerConsumption, 2, Units.WATT);
175 updateQtyState(CHANNEL_SENEC_ENERGY_PRODUCTION, response.energy.inverterPowerGeneration, 2, Units.WATT);
176 updateQtyState(CHANNEL_SENEC_BATTERY_POWER, response.energy.batteryPower, 2, Units.WATT);
177 updateQtyState(CHANNEL_SENEC_BATTERY_CURRENT, response.energy.batteryCurrent, 2, Units.AMPERE);
178 updateQtyState(CHANNEL_SENEC_BATTERY_VOLTAGE, response.energy.batteryVoltage, 2, Units.VOLT);
179 updateStringStateFromInt(CHANNEL_SENEC_SYSTEM_STATE, response.energy.systemState,
180 SenecSystemStatus::descriptionFromCode);
181 updateDecimalState(CHANNEL_SENEC_SYSTEM_STATE_VALUE, response.energy.systemState);
182 updateQtyState(CHANNEL_SENEC_BATTERY_FUEL_CHARGE, response.energy.batteryFuelCharge, 0, Units.PERCENT);
184 updateGridPowerValues(getSenecValue(response.grid.currentGridValue));
185 updateQtyState(CHANNEL_SENEC_GRID_CURRENT_PH1, response.grid.currentGridCurrentPerPhase[0], 2,
187 updateQtyState(CHANNEL_SENEC_GRID_CURRENT_PH2, response.grid.currentGridCurrentPerPhase[1], 2,
189 updateQtyState(CHANNEL_SENEC_GRID_CURRENT_PH3, response.grid.currentGridCurrentPerPhase[2], 2,
191 updateQtyState(CHANNEL_SENEC_GRID_POWER_PH1, response.grid.currentGridPowerPerPhase[0], 2, Units.WATT);
192 updateQtyState(CHANNEL_SENEC_GRID_POWER_PH2, response.grid.currentGridPowerPerPhase[1], 2, Units.WATT);
193 updateQtyState(CHANNEL_SENEC_GRID_POWER_PH3, response.grid.currentGridPowerPerPhase[2], 2, Units.WATT);
195 updateQtyState(CHANNEL_SENEC_GRID_VOLTAGE_PH1, response.grid.currentGridVoltagePerPhase[0], 2, Units.VOLT);
196 updateQtyState(CHANNEL_SENEC_GRID_VOLTAGE_PH2, response.grid.currentGridVoltagePerPhase[1], 2, Units.VOLT);
197 updateQtyState(CHANNEL_SENEC_GRID_VOLTAGE_PH3, response.grid.currentGridVoltagePerPhase[2], 2, Units.VOLT);
198 updateQtyState(CHANNEL_SENEC_GRID_FREQUENCY, response.grid.currentGridFrequency, 2, Units.HERTZ);
200 updateQtyState(CHANNEL_SENEC_LIVE_BAT_CHARGE, response.statistics.liveBatCharge, 2, Units.KILOWATT_HOUR);
201 updateQtyState(CHANNEL_SENEC_LIVE_BAT_DISCHARGE, response.statistics.liveBatDischarge, 2,
202 Units.KILOWATT_HOUR);
203 updateQtyState(CHANNEL_SENEC_LIVE_GRID_IMPORT, response.statistics.liveGridImport, 2, Units.KILOWATT_HOUR);
204 updateQtyState(CHANNEL_SENEC_LIVE_GRID_EXPORT, response.statistics.liveGridExport, 2, Units.KILOWATT_HOUR);
205 updateQtyState(CHANNEL_SENEC_LIVE_HOUSE_CONSUMPTION, response.statistics.liveHouseConsumption, 2,
206 Units.KILOWATT_HOUR);
207 updateQtyState(CHANNEL_SENEC_LIVE_POWER_GENERATOR, response.statistics.livePowerGenerator, 2,
208 Units.KILOWATT_HOUR);
209 if (response.statistics.liveWallboxEnergy != null) {
210 updateQtyState(CHANNEL_SENEC_LIVE_ENERGY_WALLBOX1, response.statistics.liveWallboxEnergy[0], 2,
211 Units.KILOWATT_HOUR, DIVISOR_ISO_TO_KILO);
214 if (response.battery.chargedEnergy != null) {
215 updateQtyState(CHANNEL_SENEC_CHARGED_ENERGY_PACK1, response.battery.chargedEnergy[0], 2,
216 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
217 updateQtyState(CHANNEL_SENEC_CHARGED_ENERGY_PACK2, response.battery.chargedEnergy[1], 2,
218 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
219 updateQtyState(CHANNEL_SENEC_CHARGED_ENERGY_PACK3, response.battery.chargedEnergy[2], 2,
220 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
221 updateQtyState(CHANNEL_SENEC_CHARGED_ENERGY_PACK4, response.battery.chargedEnergy[3], 2,
222 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
224 if (response.battery.dischargedEnergy != null) {
225 updateQtyState(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK1, response.battery.dischargedEnergy[0], 2,
226 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
227 updateQtyState(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK2, response.battery.dischargedEnergy[1], 2,
228 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
229 updateQtyState(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK3, response.battery.dischargedEnergy[2], 2,
230 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
231 updateQtyState(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK4, response.battery.dischargedEnergy[3], 2,
232 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
234 if (response.battery.cycles != null) {
235 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK1, response.battery.cycles[0]);
236 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK2, response.battery.cycles[1]);
237 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK3, response.battery.cycles[2]);
238 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK4, response.battery.cycles[3]);
240 if (response.battery.current != null) {
241 updateQtyState(CHANNEL_SENEC_CURRENT_PACK1, response.battery.current[0], 2, Units.AMPERE);
242 updateQtyState(CHANNEL_SENEC_CURRENT_PACK2, response.battery.current[1], 2, Units.AMPERE);
243 updateQtyState(CHANNEL_SENEC_CURRENT_PACK3, response.battery.current[2], 2, Units.AMPERE);
244 updateQtyState(CHANNEL_SENEC_CURRENT_PACK4, response.battery.current[3], 2, Units.AMPERE);
246 if (response.battery.voltage != null) {
247 updateQtyState(CHANNEL_SENEC_VOLTAGE_PACK1, response.battery.voltage[0], 2, Units.VOLT);
248 updateQtyState(CHANNEL_SENEC_VOLTAGE_PACK2, response.battery.voltage[1], 2, Units.VOLT);
249 updateQtyState(CHANNEL_SENEC_VOLTAGE_PACK3, response.battery.voltage[2], 2, Units.VOLT);
250 updateQtyState(CHANNEL_SENEC_VOLTAGE_PACK4, response.battery.voltage[3], 2, Units.VOLT);
252 if (response.battery.maxCellVoltage != null) {
253 updateQtyState(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK1, response.battery.maxCellVoltage[0], 3, Units.VOLT,
254 DIVISOR_MILLI_TO_ISO);
255 updateQtyState(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK2, response.battery.maxCellVoltage[1], 3, Units.VOLT,
256 DIVISOR_MILLI_TO_ISO);
257 updateQtyState(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK3, response.battery.maxCellVoltage[2], 3, Units.VOLT,
258 DIVISOR_MILLI_TO_ISO);
259 updateQtyState(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK4, response.battery.maxCellVoltage[3], 3, Units.VOLT,
260 DIVISOR_MILLI_TO_ISO);
262 if (response.battery.minCellVoltage != null) {
263 updateQtyState(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK1, response.battery.minCellVoltage[0], 3, Units.VOLT,
264 DIVISOR_MILLI_TO_ISO);
265 updateQtyState(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK2, response.battery.minCellVoltage[1], 3, Units.VOLT,
266 DIVISOR_MILLI_TO_ISO);
267 updateQtyState(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK3, response.battery.minCellVoltage[2], 3, Units.VOLT,
268 DIVISOR_MILLI_TO_ISO);
269 updateQtyState(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK4, response.battery.minCellVoltage[3], 3, Units.VOLT,
270 DIVISOR_MILLI_TO_ISO);
273 if (response.temperature != null) {
274 updateQtyState(CHANNEL_SENEC_BATTERY_TEMPERATURE, response.temperature.batteryTemperature, 0,
276 updateQtyState(CHANNEL_SENEC_CASE_TEMPERATURE, response.temperature.caseTemperature, 0,
278 updateQtyState(CHANNEL_SENEC_MCU_TEMPERATURE, response.temperature.mcuTemperature, 0, SIUnits.CELSIUS);
281 if (response.wallbox != null && response.wallbox.state != null) {
282 updateStringStateFromInt(CHANNEL_SENEC_WALLBOX1_STATE, response.wallbox.state[0],
283 SenecWallboxStatus::descriptionFromCode);
284 updateDecimalState(CHANNEL_SENEC_WALLBOX1_STATE_VALUE, response.wallbox.state[0]);
285 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH1, response.wallbox.l1ChargingCurrent[0], 2,
287 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH2, response.wallbox.l2ChargingCurrent[0], 2,
289 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH3, response.wallbox.l3ChargingCurrent[0], 2,
291 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_POWER, response.wallbox.chargingPower[0], 2, Units.WATT);
294 updateStatus(ThingStatus.ONLINE);
295 } catch (JsonParseException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
296 if (response == null) {
297 logger.trace("Faulty response: is null");
299 logger.trace("Faulty response: {}", response.toString());
301 logger.warn("Error refreshing source '{}'", getThing().getUID(), e);
302 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
303 "Could not connect to Senec web interface:" + e.getMessage());
309 protected void updateStringStateFromInt(String channelName, String senecValue,
310 Function<Integer, String> converter) {
311 Channel channel = getThing().getChannel(channelName);
312 if (channel != null) {
313 Integer value = getSenecValue(senecValue).intValue();
314 updateState(channel.getUID(), new StringType(converter.apply(value)));
318 protected void updateDecimalState(String channelName, String senecValue) {
319 Channel channel = getThing().getChannel(channelName);
320 if (channel != null) {
321 BigDecimal value = getSenecValue(senecValue);
322 updateState(channel.getUID(), new DecimalType(value.intValue()));
326 protected <Q extends Quantity<Q>> void updateQtyState(String channelName, String senecValue, int scale,
328 updateQtyState(channelName, senecValue, scale, unit, null);
331 protected <Q extends Quantity<Q>> void updateQtyState(String channelName, String senecValue, int scale,
332 Unit<Q> unit, @Nullable BigDecimal divisor) {
333 Channel channel = getThing().getChannel(channelName);
334 if (channel == null) {
337 BigDecimal value = getSenecValue(senecValue);
338 if (divisor != null) {
339 value = value.divide(divisor, scale, RoundingMode.HALF_UP);
341 value = value.setScale(scale, RoundingMode.HALF_UP);
343 updateState(channel.getUID(), new QuantityType<Q>(value, unit));
346 protected BigDecimal getSenecValue(String value) {
347 String[] type = value.split("_");
349 if (type[0] != null) {
350 if (type[0].startsWith(VALUE_TYPE_DECIMAL)) {
351 return new BigDecimal(Long.valueOf(type[1], 16));
352 } else if (type[0].startsWith(VALUE_TYPE_INT1)) {
353 Integer val = Integer.valueOf(type[1], 16);
354 if ((val & 0x8000) > 0) {
357 return new BigDecimal(val);
358 } else if (type[0].startsWith(VALUE_TYPE_INT3)) {
359 Long val = Long.valueOf(type[1], 16);
360 if ((Math.abs(val & 0x80000000)) > 0) {
361 val = val - 0x100000000L;
363 return new BigDecimal(val);
364 } else if (type[0].startsWith(VALUE_TYPE_INT8)) {
365 Long val = Long.valueOf(type[1], 16);
366 if ((val & 0x80) > 0) {
369 return new BigDecimal(val);
370 } else if (VALUE_TYPE_FLOAT.equalsIgnoreCase(type[0])) {
371 return parseFloatValue(type[1]);
375 logger.warn("Unknown value type [{}]", type[0]);
376 return BigDecimal.ZERO;
380 * Parse the hex coded float value of Senec device and return as BigDecimal
382 * @param value String with hex float
383 * @return BigDecimal with float value
385 private static BigDecimal parseFloatValue(String value) {
386 // sample: value = 43E26188
388 float f = Float.intBitsToFloat(Integer.parseUnsignedInt(value, 16));
389 return new BigDecimal(f);
392 protected void updatePowerLimitationStatus(Channel channel, boolean status, int duration) {
393 if (this.limitationStatus != null) {
394 if (this.limitationStatus.state == status) {
395 long stateSince = new Date().getTime() - this.limitationStatus.time;
397 if (((int) (stateSince / 1000)) < duration) {
398 // skip updating state (possible flapping state)
401 logger.debug("{} longer than required duration {}", status, duration);
404 this.limitationStatus.state = status;
405 this.limitationStatus.time = new Date().getTime();
407 // skip updating state (state changed, possible flapping state)
411 this.limitationStatus = new PowerLimitationStatusDTO();
412 this.limitationStatus.state = status;
415 logger.debug("Updating power limitation state {}", status);
416 updateState(channel.getUID(), status ? OnOffType.ON : OnOffType.OFF);
419 protected void updateGridPowerValues(BigDecimal gridTotalValue) {
420 BigDecimal gridTotal = gridTotalValue.setScale(2, RoundingMode.HALF_UP);
422 Channel channelGridPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER);
423 if (channelGridPower != null) {
424 updateState(channelGridPower.getUID(), new QuantityType<>(gridTotal, Units.WATT));
427 Channel channelGridPowerSupply = getThing()
428 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_SUPPLY);
429 if (channelGridPowerSupply != null) {
430 BigDecimal gridSupply = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? gridTotal.abs() : BigDecimal.ZERO;
431 updateState(channelGridPowerSupply.getUID(), new QuantityType<>(gridSupply, Units.WATT));
434 Channel channelGridPowerDraw = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_DRAW);
435 if (channelGridPowerDraw != null) {
436 BigDecimal gridDraw = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : gridTotal.abs();
437 updateState(channelGridPowerDraw.getUID(), new QuantityType<>(gridDraw, Units.WATT));