2 * Copyright (c) 2010-2024 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("%s://%s".formatted(config.useHttp ? "http" : "https", 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 if (response.battery.chargedEnergy != null) {
201 updateQtyState(CHANNEL_SENEC_CHARGED_ENERGY_PACK1, response.battery.chargedEnergy[0], 2,
202 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
203 updateQtyState(CHANNEL_SENEC_CHARGED_ENERGY_PACK2, response.battery.chargedEnergy[1], 2,
204 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
205 updateQtyState(CHANNEL_SENEC_CHARGED_ENERGY_PACK3, response.battery.chargedEnergy[2], 2,
206 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
207 updateQtyState(CHANNEL_SENEC_CHARGED_ENERGY_PACK4, response.battery.chargedEnergy[3], 2,
208 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
210 if (response.battery.dischargedEnergy != null) {
211 updateQtyState(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK1, response.battery.dischargedEnergy[0], 2,
212 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
213 updateQtyState(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK2, response.battery.dischargedEnergy[1], 2,
214 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
215 updateQtyState(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK3, response.battery.dischargedEnergy[2], 2,
216 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
217 updateQtyState(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK4, response.battery.dischargedEnergy[3], 2,
218 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
220 if (response.battery.cycles != null) {
221 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK1, response.battery.cycles[0]);
222 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK2, response.battery.cycles[1]);
223 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK3, response.battery.cycles[2]);
224 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK4, response.battery.cycles[3]);
226 if (response.battery.current != null) {
227 updateQtyState(CHANNEL_SENEC_CURRENT_PACK1, response.battery.current[0], 2, Units.AMPERE);
228 updateQtyState(CHANNEL_SENEC_CURRENT_PACK2, response.battery.current[1], 2, Units.AMPERE);
229 updateQtyState(CHANNEL_SENEC_CURRENT_PACK3, response.battery.current[2], 2, Units.AMPERE);
230 updateQtyState(CHANNEL_SENEC_CURRENT_PACK4, response.battery.current[3], 2, Units.AMPERE);
232 if (response.battery.voltage != null) {
233 updateQtyState(CHANNEL_SENEC_VOLTAGE_PACK1, response.battery.voltage[0], 2, Units.VOLT);
234 updateQtyState(CHANNEL_SENEC_VOLTAGE_PACK2, response.battery.voltage[1], 2, Units.VOLT);
235 updateQtyState(CHANNEL_SENEC_VOLTAGE_PACK3, response.battery.voltage[2], 2, Units.VOLT);
236 updateQtyState(CHANNEL_SENEC_VOLTAGE_PACK4, response.battery.voltage[3], 2, Units.VOLT);
238 if (response.battery.maxCellVoltage != null) {
239 updateQtyState(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK1, response.battery.maxCellVoltage[0], 3, Units.VOLT,
240 DIVISOR_MILLI_TO_ISO);
241 updateQtyState(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK2, response.battery.maxCellVoltage[1], 3, Units.VOLT,
242 DIVISOR_MILLI_TO_ISO);
243 updateQtyState(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK3, response.battery.maxCellVoltage[2], 3, Units.VOLT,
244 DIVISOR_MILLI_TO_ISO);
245 updateQtyState(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK4, response.battery.maxCellVoltage[3], 3, Units.VOLT,
246 DIVISOR_MILLI_TO_ISO);
248 if (response.battery.minCellVoltage != null) {
249 updateQtyState(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK1, response.battery.minCellVoltage[0], 3, Units.VOLT,
250 DIVISOR_MILLI_TO_ISO);
251 updateQtyState(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK2, response.battery.minCellVoltage[1], 3, Units.VOLT,
252 DIVISOR_MILLI_TO_ISO);
253 updateQtyState(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK3, response.battery.minCellVoltage[2], 3, Units.VOLT,
254 DIVISOR_MILLI_TO_ISO);
255 updateQtyState(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK4, response.battery.minCellVoltage[3], 3, Units.VOLT,
256 DIVISOR_MILLI_TO_ISO);
259 if (response.temperature != null) {
260 updateQtyState(CHANNEL_SENEC_BATTERY_TEMPERATURE, response.temperature.batteryTemperature, 0,
262 updateQtyState(CHANNEL_SENEC_CASE_TEMPERATURE, response.temperature.caseTemperature, 0,
264 updateQtyState(CHANNEL_SENEC_MCU_TEMPERATURE, response.temperature.mcuTemperature, 0, SIUnits.CELSIUS);
267 if (response.wallbox != null && response.wallbox.state != null) {
268 updateStringStateFromInt(CHANNEL_SENEC_WALLBOX1_STATE, response.wallbox.state[0],
269 SenecWallboxStatus::descriptionFromCode);
270 updateDecimalState(CHANNEL_SENEC_WALLBOX1_STATE_VALUE, response.wallbox.state[0]);
271 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH1, response.wallbox.l1ChargingCurrent[0], 2,
273 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH2, response.wallbox.l2ChargingCurrent[0], 2,
275 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH3, response.wallbox.l3ChargingCurrent[0], 2,
277 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_POWER, response.wallbox.chargingPower[0], 2, Units.WATT);
280 updateStatus(ThingStatus.ONLINE);
281 } catch (JsonParseException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
282 if (response == null) {
283 logger.trace("Faulty response: is null");
285 logger.trace("Faulty response: {}", response.toString());
287 logger.warn("Error refreshing source '{}'", getThing().getUID(), e);
288 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
289 "Could not connect to Senec web interface:" + e.getMessage());
295 protected void updateStringStateFromInt(String channelName, String senecValue,
296 Function<Integer, String> converter) {
297 Channel channel = getThing().getChannel(channelName);
298 if (channel != null) {
299 Integer value = getSenecValue(senecValue).intValue();
300 updateState(channel.getUID(), new StringType(converter.apply(value)));
304 protected void updateDecimalState(String channelName, String senecValue) {
305 Channel channel = getThing().getChannel(channelName);
306 if (channel != null) {
307 BigDecimal value = getSenecValue(senecValue);
308 updateState(channel.getUID(), new DecimalType(value.intValue()));
312 protected <Q extends Quantity<Q>> void updateQtyState(String channelName, String senecValue, int scale,
314 updateQtyState(channelName, senecValue, scale, unit, null);
317 protected <Q extends Quantity<Q>> void updateQtyState(String channelName, String senecValue, int scale,
318 Unit<Q> unit, @Nullable BigDecimal divisor) {
319 Channel channel = getThing().getChannel(channelName);
320 if (channel == null) {
323 BigDecimal value = getSenecValue(senecValue);
324 if (divisor != null) {
325 value = value.divide(divisor, scale, RoundingMode.HALF_UP);
327 value = value.setScale(scale, RoundingMode.HALF_UP);
329 updateState(channel.getUID(), new QuantityType<>(value, unit));
332 protected BigDecimal getSenecValue(String value) {
333 String[] type = value.split("_");
335 if (type[0] != null) {
336 if (type[0].startsWith(VALUE_TYPE_DECIMAL)) {
337 return new BigDecimal(Long.valueOf(type[1], 16));
338 } else if (type[0].startsWith(VALUE_TYPE_INT1)) {
339 Integer val = Integer.valueOf(type[1], 16);
340 if ((val & 0x8000) > 0) {
343 return new BigDecimal(val);
344 } else if (type[0].startsWith(VALUE_TYPE_INT3)) {
345 Long val = Long.valueOf(type[1], 16);
346 if ((Math.abs(val & 0x80000000)) > 0) {
347 val = val - 0x100000000L;
349 return new BigDecimal(val);
350 } else if (type[0].startsWith(VALUE_TYPE_INT8)) {
351 Long val = Long.valueOf(type[1], 16);
352 if ((val & 0x80) > 0) {
355 return new BigDecimal(val);
356 } else if (VALUE_TYPE_FLOAT.equalsIgnoreCase(type[0])) {
357 return parseFloatValue(type[1]);
361 logger.warn("Unknown value type [{}]", type[0]);
362 return BigDecimal.ZERO;
366 * Parse the hex coded float value of Senec device and return as BigDecimal
368 * @param value String with hex float
369 * @return BigDecimal with float value
371 private static BigDecimal parseFloatValue(String value) {
372 // sample: value = 43E26188
374 float f = Float.intBitsToFloat(Integer.parseUnsignedInt(value, 16));
375 return new BigDecimal(f);
378 protected void updatePowerLimitationStatus(Channel channel, boolean status, int duration) {
379 if (this.limitationStatus != null) {
380 if (this.limitationStatus.state == status) {
381 long stateSince = new Date().getTime() - this.limitationStatus.time;
383 if (((int) (stateSince / 1000)) < duration) {
384 // skip updating state (possible flapping state)
387 logger.debug("{} longer than required duration {}", status, duration);
390 this.limitationStatus.state = status;
391 this.limitationStatus.time = new Date().getTime();
393 // skip updating state (state changed, possible flapping state)
397 this.limitationStatus = new PowerLimitationStatusDTO();
398 this.limitationStatus.state = status;
401 logger.debug("Updating power limitation state {}", status);
402 updateState(channel.getUID(), OnOffType.from(status));
405 protected void updateGridPowerValues(BigDecimal gridTotalValue) {
406 BigDecimal gridTotal = gridTotalValue.setScale(2, RoundingMode.HALF_UP);
408 Channel channelGridPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER);
409 if (channelGridPower != null) {
410 updateState(channelGridPower.getUID(), new QuantityType<>(gridTotal, Units.WATT));
413 Channel channelGridPowerSupply = getThing()
414 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_SUPPLY);
415 if (channelGridPowerSupply != null) {
416 BigDecimal gridSupply = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? gridTotal.abs() : BigDecimal.ZERO;
417 updateState(channelGridPowerSupply.getUID(), new QuantityType<>(gridSupply, Units.WATT));
420 Channel channelGridPowerDraw = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_DRAW);
421 if (channelGridPowerDraw != null) {
422 BigDecimal gridDraw = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : gridTotal.abs();
423 updateState(channelGridPowerDraw.getUID(), new QuantityType<>(gridDraw, Units.WATT));