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.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 org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.sonnen.internal.communication.SonnenJSONCommunication;
25 import org.openhab.binding.sonnen.internal.communication.SonnenJsonDataDTO;
26 import org.openhab.binding.sonnen.internal.communication.SonnenJsonPowerMeterDataDTO;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.QuantityType;
29 import org.openhab.core.library.unit.Units;
30 import org.openhab.core.thing.Channel;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.binding.BaseThingHandler;
36 import org.openhab.core.types.Command;
37 import org.openhab.core.types.RefreshType;
38 import org.openhab.core.types.State;
39 import org.openhab.core.types.UnDefType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * The {@link SonnenHandler} is responsible for handling commands, which are
45 * sent to one of the channels.
47 * @author Christian Feininger - Initial contribution
50 public class SonnenHandler extends BaseThingHandler {
52 private final Logger logger = LoggerFactory.getLogger(SonnenHandler.class);
54 private SonnenConfiguration config = new SonnenConfiguration();
56 private @Nullable ScheduledFuture<?> refreshJob;
58 private SonnenJSONCommunication serviceCommunication;
60 private boolean automaticRefreshing = false;
62 private boolean sonnenAPIV2 = false;
64 private int disconnectionCounter = 0;
66 private Map<String, Boolean> linkedChannels = new HashMap<>();
68 public SonnenHandler(Thing thing) {
70 serviceCommunication = new SonnenJSONCommunication();
74 public void initialize() {
75 logger.debug("Initializing sonnen handler for thing {}", getThing().getUID());
76 config = getConfigAs(SonnenConfiguration.class);
77 if (config.refreshInterval < 0 || config.refreshInterval > 1000) {
78 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
79 "Parameter 'refresh Rate' msut be in the range 0-1000!");
82 if (config.hostIP == null) {
83 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP Address must be configured!");
87 if (!config.authToken.isEmpty()) {
91 serviceCommunication.setConfig(config);
92 updateStatus(ThingStatus.UNKNOWN);
93 scheduler.submit(() -> {
94 if (updateBatteryData()) {
95 for (Channel channel : getThing().getChannels()) {
96 if (isLinked(channel.getUID().getId())) {
97 channelLinked(channel.getUID());
105 * Calls the service to update the battery data
107 * @return true if the update succeeded, false otherwise
109 private boolean updateBatteryData() {
112 error = serviceCommunication.refreshBatteryConnectionAPICALLV2(arePowerMeterChannelsLinked());
114 error = serviceCommunication.refreshBatteryConnectionAPICALLV1();
116 if (error.isEmpty()) {
117 if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
118 updateStatus(ThingStatus.ONLINE);
119 disconnectionCounter = 0;
122 disconnectionCounter++;
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
124 if (disconnectionCounter < 60) {
128 return error.isEmpty();
131 private void verifyLinkedChannel(String channelID) {
132 if (isLinked(channelID) && !linkedChannels.containsKey(channelID)) {
133 linkedChannels.put(channelID, true);
138 public void dispose() {
139 stopAutomaticRefresh();
140 linkedChannels.clear();
141 automaticRefreshing = false;
144 private void stopAutomaticRefresh() {
145 ScheduledFuture<?> job = refreshJob;
153 * Start the job refreshing the battery status
155 private void startAutomaticRefresh() {
156 ScheduledFuture<?> job = refreshJob;
157 if (job == null || job.isCancelled()) {
158 refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, 0, config.refreshInterval,
163 private void refreshChannels() {
165 for (Channel channel : getThing().getChannels()) {
166 updateChannel(channel.getUID().getId());
171 public void channelLinked(ChannelUID channelUID) {
172 if (!automaticRefreshing) {
173 logger.debug("Start automatic refreshing");
174 startAutomaticRefresh();
175 automaticRefreshing = true;
177 verifyLinkedChannel(channelUID.getId());
178 updateChannel(channelUID.getId());
182 public void channelUnlinked(ChannelUID channelUID) {
183 linkedChannels.remove(channelUID.getId());
184 if (linkedChannels.isEmpty()) {
185 automaticRefreshing = false;
186 stopAutomaticRefresh();
187 logger.debug("Stop automatic refreshing");
191 private void updateChannel(String channelId) {
192 if (isLinked(channelId)) {
194 SonnenJsonDataDTO data = serviceCommunication.getBatteryData();
195 // The sonnen API has two sub-channels, e.g. 4_1 and 4_2, one representing consumption and the
196 // other production. E.g. 4_1.kwh_imported represents the total production since the
197 // battery was installed.
198 SonnenJsonPowerMeterDataDTO[] dataPM = null;
199 if (arePowerMeterChannelsLinked()) {
200 dataPM = serviceCommunication.getPowerMeterData();
203 if (dataPM != null && dataPM.length >= 2) {
205 case CHANNELENERGYIMPORTEDSTATEPRODUCTION:
206 state = new QuantityType<>(dataPM[0].getKwhImported(), Units.KILOWATT_HOUR);
207 update(state, channelId);
209 case CHANNELENERGYEXPORTEDSTATEPRODUCTION:
210 state = new QuantityType<>(dataPM[0].getKwhExported(), Units.KILOWATT_HOUR);
211 update(state, channelId);
213 case CHANNELENERGYIMPORTEDSTATECONSUMPTION:
214 state = new QuantityType<>(dataPM[1].getKwhImported(), Units.KILOWATT_HOUR);
215 update(state, channelId);
217 case CHANNELENERGYEXPORTEDSTATECONSUMPTION:
218 state = new QuantityType<>(dataPM[1].getKwhExported(), Units.KILOWATT_HOUR);
219 update(state, channelId);
226 case CHANNELBATTERYDISCHARGINGSTATE:
227 update(OnOffType.from(data.isBatteryDischarging()), channelId);
229 case CHANNELBATTERYCHARGINGSTATE:
230 update(OnOffType.from(data.isBatteryCharging()), channelId);
232 case CHANNELCONSUMPTION:
233 state = new QuantityType<>(data.getConsumptionHouse(), Units.WATT);
234 update(state, channelId);
236 case CHANNELBATTERYDISCHARGING:
237 state = new QuantityType<>(data.getbatteryCurrent() > 0 ? data.getbatteryCurrent() : 0,
239 update(state, channelId);
241 case CHANNELBATTERYCHARGING:
242 state = new QuantityType<>(data.getbatteryCurrent() <= 0 ? (data.getbatteryCurrent() * -1) : 0,
244 update(state, channelId);
246 case CHANNELGRIDFEEDIN:
247 state = new QuantityType<>(data.getGridValue() > 0 ? data.getGridValue() : 0, Units.WATT);
248 update(state, channelId);
250 case CHANNELGRIDCONSUMPTION:
251 state = new QuantityType<>(data.getGridValue() <= 0 ? (data.getGridValue() * -1) : 0,
253 update(state, channelId);
255 case CHANNELSOLARPRODUCTION:
256 state = new QuantityType<>(data.getSolarProduction(), Units.WATT);
257 update(state, channelId);
259 case CHANNELBATTERYLEVEL:
260 state = new QuantityType<>(data.getBatteryChargingLevel(), Units.PERCENT);
261 update(state, channelId);
263 case CHANNELFLOWCONSUMPTIONBATTERYSTATE:
264 update(OnOffType.from(data.isFlowConsumptionBattery()), channelId);
266 case CHANNELFLOWCONSUMPTIONGRIDSTATE:
267 update(OnOffType.from(data.isFlowConsumptionGrid()), channelId);
269 case CHANNELFLOWCONSUMPTIONPRODUCTIONSTATE:
270 update(OnOffType.from(data.isFlowConsumptionProduction()), channelId);
272 case CHANNELFLOWGRIDBATTERYSTATE:
273 update(OnOffType.from(data.isFlowGridBattery()), channelId);
275 case CHANNELFLOWPRODUCTIONBATTERYSTATE:
276 update(OnOffType.from(data.isFlowProductionBattery()), channelId);
278 case CHANNELFLOWPRODUCTIONGRIDSTATE:
279 update(OnOffType.from(data.isFlowProductionGrid()), channelId);
284 update(null, channelId);
288 @SuppressWarnings("PMD.SimplifyBooleanReturns")
289 private boolean arePowerMeterChannelsLinked() {
290 if (isLinked(CHANNELENERGYIMPORTEDSTATEPRODUCTION)) {
292 } else if (isLinked(CHANNELENERGYEXPORTEDSTATEPRODUCTION)) {
294 } else if (isLinked(CHANNELENERGYIMPORTEDSTATECONSUMPTION)) {
296 } else if (isLinked(CHANNELENERGYEXPORTEDSTATECONSUMPTION)) {
304 * Updates the State of the given channel
306 * @param state Given state
307 * @param channelId the refereed channelID
309 private void update(@Nullable State state, String channelId) {
310 logger.debug("Update channel {} with state {}", channelId, (state == null) ? "null" : state.toString());
311 updateState(channelId, state != null ? state : UnDefType.UNDEF);
315 public void handleCommand(ChannelUID channelUID, Command command) {
316 if (command == RefreshType.REFRESH) {
318 updateChannel(channelUID.getId());