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.sonnen.internal;
15 import static org.openhab.binding.sonnen.internal.SonnenBindingConstants.*;
17 import java.util.HashMap;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
22 import javax.measure.quantity.Dimensionless;
23 import javax.measure.quantity.Energy;
24 import javax.measure.quantity.Power;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.sonnen.internal.communication.SonnenJSONCommunication;
29 import org.openhab.binding.sonnen.internal.communication.SonnenJsonDataDTO;
30 import org.openhab.binding.sonnen.internal.communication.SonnenJsonPowerMeterDataDTO;
31 import org.openhab.core.library.types.OnOffType;
32 import org.openhab.core.library.types.QuantityType;
33 import org.openhab.core.library.unit.Units;
34 import org.openhab.core.thing.Channel;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.State;
43 import org.openhab.core.types.UnDefType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link SonnenHandler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author Christian Feininger - Initial contribution
54 public class SonnenHandler extends BaseThingHandler {
56 private final Logger logger = LoggerFactory.getLogger(SonnenHandler.class);
58 private SonnenConfiguration config = new SonnenConfiguration();
60 private @Nullable ScheduledFuture<?> refreshJob;
62 private SonnenJSONCommunication serviceCommunication;
64 private boolean automaticRefreshing = false;
66 private boolean sonnenAPIV2 = false;
68 private int disconnectionCounter = 0;
70 private Map<String, Boolean> linkedChannels = new HashMap<>();
72 public SonnenHandler(Thing thing) {
74 serviceCommunication = new SonnenJSONCommunication();
78 public void initialize() {
79 logger.debug("Initializing sonnen handler for thing {}", getThing().getUID());
80 config = getConfigAs(SonnenConfiguration.class);
81 if (config.refreshInterval < 0 || config.refreshInterval > 1000) {
82 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
83 "Parameter 'refresh Rate' msut be in the range 0-1000!");
86 if (config.hostIP == null) {
87 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP Address must be configured!");
91 if (!config.authToken.isEmpty()) {
95 serviceCommunication.setConfig(config);
96 updateStatus(ThingStatus.UNKNOWN);
97 scheduler.submit(() -> {
98 if (updateBatteryData()) {
99 for (Channel channel : getThing().getChannels()) {
100 if (isLinked(channel.getUID().getId())) {
101 channelLinked(channel.getUID());
109 * Calls the service to update the battery data
111 * @return true if the update succeeded, false otherwise
113 private boolean updateBatteryData() {
116 error = serviceCommunication.refreshBatteryConnectionAPICALLV2(arePowerMeterChannelsLinked());
118 error = serviceCommunication.refreshBatteryConnectionAPICALLV1();
120 if (error.isEmpty()) {
121 if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
122 updateStatus(ThingStatus.ONLINE);
123 disconnectionCounter = 0;
126 disconnectionCounter++;
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
128 if (disconnectionCounter < 60) {
132 return error.isEmpty();
135 private void verifyLinkedChannel(String channelID) {
136 if (isLinked(channelID) && !linkedChannels.containsKey(channelID)) {
137 linkedChannels.put(channelID, true);
142 public void dispose() {
143 stopAutomaticRefresh();
144 linkedChannels.clear();
145 automaticRefreshing = false;
148 private void stopAutomaticRefresh() {
149 ScheduledFuture<?> job = refreshJob;
157 * Start the job refreshing the battery status
159 private void startAutomaticRefresh() {
160 ScheduledFuture<?> job = refreshJob;
161 if (job == null || job.isCancelled()) {
162 refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, 0, config.refreshInterval,
167 private void refreshChannels() {
169 for (Channel channel : getThing().getChannels()) {
170 updateChannel(channel.getUID().getId());
175 public void channelLinked(ChannelUID channelUID) {
176 if (!automaticRefreshing) {
177 logger.debug("Start automatic refreshing");
178 startAutomaticRefresh();
179 automaticRefreshing = true;
181 verifyLinkedChannel(channelUID.getId());
182 updateChannel(channelUID.getId());
186 public void channelUnlinked(ChannelUID channelUID) {
187 linkedChannels.remove(channelUID.getId());
188 if (linkedChannels.isEmpty()) {
189 automaticRefreshing = false;
190 stopAutomaticRefresh();
191 logger.debug("Stop automatic refreshing");
195 private void updateChannel(String channelId) {
196 if (isLinked(channelId)) {
198 SonnenJsonDataDTO data = serviceCommunication.getBatteryData();
199 // The sonnen API has two sub-channels, e.g. 4_1 and 4_2, one representing consumption and the
200 // other production. E.g. 4_1.kwh_imported represents the total production since the
201 // battery was installed.
202 SonnenJsonPowerMeterDataDTO[] dataPM = null;
203 if (arePowerMeterChannelsLinked()) {
204 dataPM = serviceCommunication.getPowerMeterData();
207 if (dataPM != null && dataPM.length >= 2) {
209 case CHANNELENERGYIMPORTEDSTATEPRODUCTION:
210 state = new QuantityType<Energy>(dataPM[0].getKwhImported(), Units.KILOWATT_HOUR);
211 update(state, channelId);
213 case CHANNELENERGYEXPORTEDSTATEPRODUCTION:
214 state = new QuantityType<Energy>(dataPM[0].getKwhExported(), Units.KILOWATT_HOUR);
215 update(state, channelId);
217 case CHANNELENERGYIMPORTEDSTATECONSUMPTION:
218 state = new QuantityType<Energy>(dataPM[1].getKwhImported(), Units.KILOWATT_HOUR);
219 update(state, channelId);
221 case CHANNELENERGYEXPORTEDSTATECONSUMPTION:
222 state = new QuantityType<Energy>(dataPM[1].getKwhExported(), Units.KILOWATT_HOUR);
223 update(state, channelId);
230 case CHANNELBATTERYDISCHARGINGSTATE:
231 update(OnOffType.from(data.isBatteryDischarging()), channelId);
233 case CHANNELBATTERYCHARGINGSTATE:
234 update(OnOffType.from(data.isBatteryCharging()), channelId);
236 case CHANNELCONSUMPTION:
237 state = new QuantityType<Power>(data.getConsumptionHouse(), Units.WATT);
238 update(state, channelId);
240 case CHANNELBATTERYDISCHARGING:
241 state = new QuantityType<Power>(data.getbatteryCurrent() > 0 ? data.getbatteryCurrent() : 0,
243 update(state, channelId);
245 case CHANNELBATTERYCHARGING:
246 state = new QuantityType<Power>(
247 data.getbatteryCurrent() <= 0 ? (data.getbatteryCurrent() * -1) : 0, Units.WATT);
248 update(state, channelId);
250 case CHANNELGRIDFEEDIN:
251 state = new QuantityType<Power>(data.getGridValue() > 0 ? data.getGridValue() : 0, Units.WATT);
252 update(state, channelId);
254 case CHANNELGRIDCONSUMPTION:
255 state = new QuantityType<Power>(data.getGridValue() <= 0 ? (data.getGridValue() * -1) : 0,
257 update(state, channelId);
259 case CHANNELSOLARPRODUCTION:
260 state = new QuantityType<Power>(data.getSolarProduction(), Units.WATT);
261 update(state, channelId);
263 case CHANNELBATTERYLEVEL:
264 state = new QuantityType<Dimensionless>(data.getBatteryChargingLevel(), Units.PERCENT);
265 update(state, channelId);
267 case CHANNELFLOWCONSUMPTIONBATTERYSTATE:
268 update(OnOffType.from(data.isFlowConsumptionBattery()), channelId);
270 case CHANNELFLOWCONSUMPTIONGRIDSTATE:
271 update(OnOffType.from(data.isFlowConsumptionGrid()), channelId);
273 case CHANNELFLOWCONSUMPTIONPRODUCTIONSTATE:
274 update(OnOffType.from(data.isFlowConsumptionProduction()), channelId);
276 case CHANNELFLOWGRIDBATTERYSTATE:
277 update(OnOffType.from(data.isFlowGridBattery()), channelId);
279 case CHANNELFLOWPRODUCTIONBATTERYSTATE:
280 update(OnOffType.from(data.isFlowProductionBattery()), channelId);
282 case CHANNELFLOWPRODUCTIONGRIDSTATE:
283 update(OnOffType.from(data.isFlowProductionGrid()), channelId);
288 update(null, channelId);
292 private boolean arePowerMeterChannelsLinked() {
293 if (isLinked(CHANNELENERGYIMPORTEDSTATEPRODUCTION)) {
295 } else if (isLinked(CHANNELENERGYEXPORTEDSTATEPRODUCTION)) {
297 } else if (isLinked(CHANNELENERGYIMPORTEDSTATECONSUMPTION)) {
299 } else if (isLinked(CHANNELENERGYEXPORTEDSTATECONSUMPTION)) {
307 * Updates the State of the given channel
309 * @param state Given state
310 * @param channelId the refereed channelID
312 private void update(@Nullable State state, String channelId) {
313 logger.debug("Update channel {} with state {}", channelId, (state == null) ? "null" : state.toString());
314 updateState(channelId, state != null ? state : UnDefType.UNDEF);
318 public void handleCommand(ChannelUID channelUID, Command command) {
319 if (command == RefreshType.REFRESH) {
321 updateChannel(channelUID.getId());