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.dto.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 // ix (x=1,3,8) types => hex encoded integer value
71 private static final String VALUE_TYPE_INT1 = "i1";
72 public static final String VALUE_TYPE_INT3 = "i3";
73 public static final String VALUE_TYPE_INT8 = "i8";
74 // ux (x=1,3,6,8) types => hex encoded unsigned value
75 private static final String VALUE_TYPE_DECIMAL = "u";
76 // fl => hex encoded float
77 private static final String VALUE_TYPE_FLOAT = "fl";
79 // public static final String VALUE_TYPE_STRING = "st";
81 private @Nullable ScheduledFuture<?> refreshJob;
82 private @Nullable PowerLimitationStatusDTO limitationStatus = null;
83 private final SenecHomeApi senecHomeApi;
84 private SenecHomeConfigurationDTO config = new SenecHomeConfigurationDTO();
85 private final ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofSeconds(5), this::refreshState);
87 public SenecHomeHandler(Thing thing, HttpClient httpClient) {
89 this.senecHomeApi = new SenecHomeApi(httpClient);
93 public void handleRemoval() {
95 super.handleRemoval();
99 public void handleCommand(ChannelUID channelUID, Command command) {
100 if (command == REFRESH) {
101 logger.debug("Refreshing {}", channelUID);
104 logger.trace("The SenecHome-Binding is a read-only binding and can not handle commands");
109 public void dispose() {
114 protected void stopJobIfRunning() {
115 final ScheduledFuture<?> refreshJob = this.refreshJob;
116 if (refreshJob != null && !refreshJob.isCancelled()) {
117 refreshJob.cancel(true);
118 this.refreshJob = null;
123 public void initialize() {
124 config = getConfigAs(SenecHomeConfigurationDTO.class);
125 senecHomeApi.setHostname("%s://%s".formatted(config.useHttp ? "http" : "https", config.hostname));
126 refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refreshInterval, TimeUnit.SECONDS);
127 limitationStatus = null;
130 private void refresh() {
131 refreshCache.getValue();
134 private <Q extends Quantity<Q>> void updateQtyStateIfAvailable(String channel, String @Nullable [] valueArray,
135 int arrayIndex, int scale, Unit<Q> unit) {
136 updateQtyStateIfAvailable(channel, valueArray, arrayIndex, scale, unit, null);
139 private <Q extends Quantity<Q>> void updateQtyStateIfAvailable(String channel, String @Nullable [] valueArray,
140 int arrayIndex, int scale, Unit<Q> unit, @Nullable BigDecimal divisor) {
141 if (valueArray != null && valueArray.length > arrayIndex) {
142 updateQtyState(channel, valueArray[arrayIndex], scale, unit, divisor);
146 public @Nullable Boolean refreshState() {
147 SenecHomeResponse response = null;
149 response = senecHomeApi.getStatistics();
150 logger.trace("received {}", response);
152 BigDecimal pvLimitation = new BigDecimal(100).subtract(getSenecValue(response.power.powerLimitation))
153 .setScale(0, RoundingMode.HALF_UP);
154 updateState(CHANNEL_SENEC_POWER_LIMITATION, new QuantityType<>(pvLimitation, Units.PERCENT));
156 Channel channelLimitationState = getThing().getChannel(CHANNEL_SENEC_POWER_LIMITATION_STATE);
157 if (channelLimitationState != null) {
158 updatePowerLimitationStatus(channelLimitationState,
159 (100 - pvLimitation.intValue()) <= config.limitationTresholdValue, config.limitationDuration);
161 if (response.power.currentPerMpp != null) {
162 updateQtyState(CHANNEL_SENEC_CURRENT_MPP1, response.power.currentPerMpp[0], 2, Units.AMPERE);
163 updateQtyState(CHANNEL_SENEC_CURRENT_MPP2, response.power.currentPerMpp[1], 2, Units.AMPERE);
164 if (response.power.currentPerMpp.length > 2) {
166 updateQtyState(CHANNEL_SENEC_CURRENT_MPP3, response.power.currentPerMpp[2], 2, Units.AMPERE);
169 if (response.power.powerPerMpp != null) {
170 updateQtyState(CHANNEL_SENEC_POWER_MPP1, response.power.powerPerMpp[0], 2, Units.WATT);
171 updateQtyState(CHANNEL_SENEC_POWER_MPP2, response.power.powerPerMpp[1], 2, Units.WATT);
172 if (response.power.powerPerMpp.length > 2) {
173 updateQtyState(CHANNEL_SENEC_POWER_MPP3, response.power.powerPerMpp[2], 2, Units.WATT);
176 if (response.power.voltagePerMpp != null) {
177 updateQtyState(CHANNEL_SENEC_VOLTAGE_MPP1, response.power.voltagePerMpp[0], 2, Units.VOLT);
178 updateQtyState(CHANNEL_SENEC_VOLTAGE_MPP2, response.power.voltagePerMpp[1], 2, Units.VOLT);
179 if (response.power.voltagePerMpp.length > 2) {
180 updateQtyState(CHANNEL_SENEC_VOLTAGE_MPP3, response.power.voltagePerMpp[2], 2, Units.VOLT);
184 updateQtyState(CHANNEL_SENEC_POWER_CONSUMPTION, response.energy.housePowerConsumption, 2, Units.WATT);
185 updateQtyState(CHANNEL_SENEC_ENERGY_PRODUCTION, response.energy.inverterPowerGeneration, 2, Units.WATT);
186 updateQtyState(CHANNEL_SENEC_BATTERY_POWER, response.energy.batteryPower, 2, Units.WATT);
187 updateQtyState(CHANNEL_SENEC_BATTERY_CURRENT, response.energy.batteryCurrent, 2, Units.AMPERE);
188 updateQtyState(CHANNEL_SENEC_BATTERY_VOLTAGE, response.energy.batteryVoltage, 2, Units.VOLT);
189 updateStringStateFromInt(CHANNEL_SENEC_SYSTEM_STATE, response.energy.systemState,
190 SenecSystemStatus::descriptionFromCode);
191 updateDecimalState(CHANNEL_SENEC_SYSTEM_STATE_VALUE, response.energy.systemState);
192 updateQtyState(CHANNEL_SENEC_BATTERY_FUEL_CHARGE, response.energy.batteryFuelCharge, 0, Units.PERCENT);
194 updateGridPowerValues(getSenecValue(response.grid.currentGridValue));
195 updateQtyState(CHANNEL_SENEC_GRID_CURRENT_PH1, response.grid.currentGridCurrentPerPhase[0], 2,
197 updateQtyState(CHANNEL_SENEC_GRID_CURRENT_PH2, response.grid.currentGridCurrentPerPhase[1], 2,
199 updateQtyState(CHANNEL_SENEC_GRID_CURRENT_PH3, response.grid.currentGridCurrentPerPhase[2], 2,
201 updateQtyState(CHANNEL_SENEC_GRID_POWER_PH1, response.grid.currentGridPowerPerPhase[0], 2, Units.WATT);
202 updateQtyState(CHANNEL_SENEC_GRID_POWER_PH2, response.grid.currentGridPowerPerPhase[1], 2, Units.WATT);
203 updateQtyState(CHANNEL_SENEC_GRID_POWER_PH3, response.grid.currentGridPowerPerPhase[2], 2, Units.WATT);
205 updateQtyState(CHANNEL_SENEC_GRID_VOLTAGE_PH1, response.grid.currentGridVoltagePerPhase[0], 2, Units.VOLT);
206 updateQtyState(CHANNEL_SENEC_GRID_VOLTAGE_PH2, response.grid.currentGridVoltagePerPhase[1], 2, Units.VOLT);
207 updateQtyState(CHANNEL_SENEC_GRID_VOLTAGE_PH3, response.grid.currentGridVoltagePerPhase[2], 2, Units.VOLT);
208 updateQtyState(CHANNEL_SENEC_GRID_FREQUENCY, response.grid.currentGridFrequency, 2, Units.HERTZ);
210 updateQtyStateIfAvailable(CHANNEL_SENEC_CHARGED_ENERGY_PACK1, response.battery.chargedEnergy, 0, 2,
211 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
212 updateQtyStateIfAvailable(CHANNEL_SENEC_CHARGED_ENERGY_PACK2, response.battery.chargedEnergy, 1, 2,
213 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
214 updateQtyStateIfAvailable(CHANNEL_SENEC_CHARGED_ENERGY_PACK3, response.battery.chargedEnergy, 2, 2,
215 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
216 updateQtyStateIfAvailable(CHANNEL_SENEC_CHARGED_ENERGY_PACK4, response.battery.chargedEnergy, 3, 2,
217 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
219 updateQtyStateIfAvailable(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK1, response.battery.dischargedEnergy, 0, 2,
220 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
221 updateQtyStateIfAvailable(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK2, response.battery.dischargedEnergy, 1, 2,
222 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
223 updateQtyStateIfAvailable(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK3, response.battery.dischargedEnergy, 2, 2,
224 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
225 updateQtyStateIfAvailable(CHANNEL_SENEC_DISCHARGED_ENERGY_PACK4, response.battery.dischargedEnergy, 3, 2,
226 Units.KILOWATT_HOUR, DIVISOR_MILLI_TO_KILO);
228 if (response.battery.cycles != null) {
229 int length = response.battery.cycles.length;
231 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK1, response.battery.cycles[0]);
234 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK2, response.battery.cycles[1]);
237 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK3, response.battery.cycles[2]);
240 updateDecimalState(CHANNEL_SENEC_CYCLES_PACK4, response.battery.cycles[3]);
244 updateQtyStateIfAvailable(CHANNEL_SENEC_CURRENT_PACK1, response.battery.current, 0, 2, Units.AMPERE);
245 updateQtyStateIfAvailable(CHANNEL_SENEC_CURRENT_PACK2, response.battery.current, 1, 2, Units.AMPERE);
246 updateQtyStateIfAvailable(CHANNEL_SENEC_CURRENT_PACK3, response.battery.current, 2, 2, Units.AMPERE);
247 updateQtyStateIfAvailable(CHANNEL_SENEC_CURRENT_PACK4, response.battery.current, 3, 2, Units.AMPERE);
249 updateQtyStateIfAvailable(CHANNEL_SENEC_VOLTAGE_PACK1, response.battery.voltage, 0, 2, Units.VOLT);
250 updateQtyStateIfAvailable(CHANNEL_SENEC_VOLTAGE_PACK2, response.battery.voltage, 1, 2, Units.VOLT);
251 updateQtyStateIfAvailable(CHANNEL_SENEC_VOLTAGE_PACK3, response.battery.voltage, 2, 2, Units.VOLT);
252 updateQtyStateIfAvailable(CHANNEL_SENEC_VOLTAGE_PACK4, response.battery.voltage, 3, 2, Units.VOLT);
254 updateQtyStateIfAvailable(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK1, response.battery.maxCellVoltage, 0, 3,
255 Units.VOLT, DIVISOR_MILLI_TO_ISO);
256 updateQtyStateIfAvailable(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK2, response.battery.maxCellVoltage, 1, 3,
257 Units.VOLT, DIVISOR_MILLI_TO_ISO);
258 updateQtyStateIfAvailable(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK3, response.battery.maxCellVoltage, 2, 3,
259 Units.VOLT, DIVISOR_MILLI_TO_ISO);
260 updateQtyStateIfAvailable(CHANNEL_SENEC_MAX_CELL_VOLTAGE_PACK4, response.battery.maxCellVoltage, 3, 3,
261 Units.VOLT, DIVISOR_MILLI_TO_ISO);
263 updateQtyStateIfAvailable(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK1, response.battery.minCellVoltage, 0, 3,
264 Units.VOLT, DIVISOR_MILLI_TO_ISO);
265 updateQtyStateIfAvailable(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK2, response.battery.minCellVoltage, 1, 3,
266 Units.VOLT, DIVISOR_MILLI_TO_ISO);
267 updateQtyStateIfAvailable(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK3, response.battery.minCellVoltage, 2, 3,
268 Units.VOLT, DIVISOR_MILLI_TO_ISO);
269 updateQtyStateIfAvailable(CHANNEL_SENEC_MIN_CELL_VOLTAGE_PACK4, response.battery.minCellVoltage, 3, 3,
270 Units.VOLT, DIVISOR_MILLI_TO_ISO);
272 if (response.temperature != null) {
273 updateQtyState(CHANNEL_SENEC_BATTERY_TEMPERATURE, response.temperature.batteryTemperature, 0,
275 updateQtyState(CHANNEL_SENEC_CASE_TEMPERATURE, response.temperature.caseTemperature, 0,
277 updateQtyState(CHANNEL_SENEC_MCU_TEMPERATURE, response.temperature.mcuTemperature, 0, SIUnits.CELSIUS);
280 if (response.wallbox != null && response.wallbox.state != null) {
281 updateStringStateFromInt(CHANNEL_SENEC_WALLBOX1_STATE, response.wallbox.state[0],
282 SenecWallboxStatus::descriptionFromCode);
283 updateDecimalState(CHANNEL_SENEC_WALLBOX1_STATE_VALUE, response.wallbox.state[0]);
284 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH1, response.wallbox.l1ChargingCurrent[0], 2,
286 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH2, response.wallbox.l2ChargingCurrent[0], 2,
288 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_CURRENT_PH3, response.wallbox.l3ChargingCurrent[0], 2,
290 updateQtyState(CHANNEL_SENEC_WALLBOX1_CHARGING_POWER, response.wallbox.chargingPower[0], 2, Units.WATT);
293 updateStatus(ThingStatus.ONLINE);
294 } catch (JsonParseException | IOException | InterruptedException | TimeoutException | ExecutionException e) {
295 if (response == null) {
296 logger.trace("Faulty response: is null");
298 logger.trace("Faulty response: {}", response.toString());
300 logger.warn("Error refreshing source '{}'", getThing().getUID(), e);
301 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
302 "Could not connect to Senec web interface:" + e.getMessage());
308 protected void updateStringStateFromInt(String channelName, String senecValue,
309 Function<Integer, String> converter) {
310 Channel channel = getThing().getChannel(channelName);
311 if (channel != null) {
312 Integer value = getSenecValue(senecValue).intValue();
313 updateState(channel.getUID(), new StringType(converter.apply(value)));
317 protected void updateDecimalState(String channelName, String senecValue) {
318 Channel channel = getThing().getChannel(channelName);
319 if (channel != null) {
320 BigDecimal value = getSenecValue(senecValue);
321 updateState(channel.getUID(), new DecimalType(value.intValue()));
325 protected <Q extends Quantity<Q>> void updateQtyState(String channelName, String senecValue, int scale,
327 updateQtyState(channelName, senecValue, scale, unit, null);
330 protected <Q extends Quantity<Q>> void updateQtyState(String channelName, String senecValue, int scale,
331 Unit<Q> unit, @Nullable BigDecimal divisor) {
332 Channel channel = getThing().getChannel(channelName);
333 if (channel == null) {
336 BigDecimal value = getSenecValue(senecValue);
337 if (divisor != null) {
338 value = value.divide(divisor, scale, RoundingMode.HALF_UP);
340 value = value.setScale(scale, RoundingMode.HALF_UP);
342 updateState(channel.getUID(), new QuantityType<>(value, unit));
345 protected BigDecimal getSenecValue(String value) {
346 String[] type = value.split("_");
348 if (type[0] != null) {
349 if (type[0].startsWith(VALUE_TYPE_DECIMAL)) {
350 return new BigDecimal(Long.valueOf(type[1], 16));
351 } else if (type[0].startsWith(VALUE_TYPE_INT1)) {
352 Integer val = Integer.valueOf(type[1], 16);
353 if ((val & 0x8000) > 0) {
356 return new BigDecimal(val);
357 } else if (type[0].startsWith(VALUE_TYPE_INT3)) {
358 Long val = Long.valueOf(type[1], 16);
359 if ((Math.abs(val & 0x80000000)) > 0) {
360 val = val - 0x100000000L;
362 return new BigDecimal(val);
363 } else if (type[0].startsWith(VALUE_TYPE_INT8)) {
364 Long val = Long.valueOf(type[1], 16);
365 if ((val & 0x80) > 0) {
368 return new BigDecimal(val);
369 } else if (VALUE_TYPE_FLOAT.equalsIgnoreCase(type[0])) {
370 return parseFloatValue(type[1]);
374 logger.warn("Unknown value type [{}]", type[0]);
375 return BigDecimal.ZERO;
379 * Parse the hex coded float value of Senec device and return as BigDecimal
381 * @param value String with hex float
382 * @return BigDecimal with float value
384 private static BigDecimal parseFloatValue(String value) {
385 // sample: value = 43E26188
387 float f = Float.intBitsToFloat(Integer.parseUnsignedInt(value, 16));
388 return new BigDecimal(f);
391 protected void updatePowerLimitationStatus(Channel channel, boolean status, int duration) {
392 PowerLimitationStatusDTO limitationStatus = this.limitationStatus;
393 if (limitationStatus != null) {
394 if (limitationStatus.state == status) {
395 long stateSince = new Date().getTime() - 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 limitationStatus.state = status;
405 limitationStatus.time = new Date().getTime();
406 this.limitationStatus = limitationStatus;
408 // skip updating state (state changed, possible flapping state)
412 limitationStatus = new PowerLimitationStatusDTO();
413 limitationStatus.state = status;
414 this.limitationStatus = limitationStatus;
417 logger.debug("Updating power limitation state {}", status);
418 updateState(channel.getUID(), OnOffType.from(status));
421 protected void updateGridPowerValues(BigDecimal gridTotalValue) {
422 BigDecimal gridTotal = gridTotalValue.setScale(2, RoundingMode.HALF_UP);
424 Channel channelGridPower = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER);
425 if (channelGridPower != null) {
426 updateState(channelGridPower.getUID(), new QuantityType<>(gridTotal, Units.WATT));
429 Channel channelGridPowerSupply = getThing()
430 .getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_SUPPLY);
431 if (channelGridPowerSupply != null) {
432 BigDecimal gridSupply = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? gridTotal.abs() : BigDecimal.ZERO;
433 updateState(channelGridPowerSupply.getUID(), new QuantityType<>(gridSupply, Units.WATT));
436 Channel channelGridPowerDraw = getThing().getChannel(SenecHomeBindingConstants.CHANNEL_SENEC_GRID_POWER_DRAW);
437 if (channelGridPowerDraw != null) {
438 BigDecimal gridDraw = gridTotal.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : gridTotal.abs();
439 updateState(channelGridPowerDraw.getUID(), new QuantityType<>(gridDraw, Units.WATT));