2 * Copyright (c) 2010-2022 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.fronius.internal.handler;
15 import java.math.BigDecimal;
17 import org.eclipse.jdt.annotation.NonNull;
18 import org.openhab.binding.fronius.internal.FroniusBridgeConfiguration;
19 import org.openhab.binding.fronius.internal.FroniusCommunicationException;
20 import org.openhab.binding.fronius.internal.FroniusHttpUtil;
21 import org.openhab.binding.fronius.internal.api.BaseFroniusResponse;
22 import org.openhab.binding.fronius.internal.api.HeadStatus;
23 import org.openhab.binding.fronius.internal.api.ValueUnit;
24 import org.openhab.core.library.types.DecimalType;
25 import org.openhab.core.library.types.QuantityType;
26 import org.openhab.core.library.types.StringType;
27 import org.openhab.core.thing.Bridge;
28 import org.openhab.core.thing.Channel;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.thing.Thing;
31 import org.openhab.core.thing.ThingStatus;
32 import org.openhab.core.thing.ThingStatusDetail;
33 import org.openhab.core.thing.binding.BaseThingHandler;
34 import org.openhab.core.types.Command;
35 import org.openhab.core.types.RefreshType;
36 import org.openhab.core.types.State;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.Gson;
41 import com.google.gson.JsonSyntaxException;
44 * Basic Handler class for all Fronius services.
46 * @author Gerrit Beine - Initial contribution
47 * @author Thomas Rokohl - Refactoring to merge the concepts
48 * @author Thomas Kordelle - Added inverter power, battery state of charge and PV solar yield
49 * @author Jimmy Tanagra - Implement connection retry
51 public abstract class FroniusBaseThingHandler extends BaseThingHandler {
53 private static final int API_TIMEOUT = 5000;
54 private final Logger logger = LoggerFactory.getLogger(FroniusBaseThingHandler.class);
55 private final String serviceDescription;
56 private FroniusBridgeHandler bridgeHandler;
57 private final Gson gson;
59 public FroniusBaseThingHandler(Thing thing) {
62 serviceDescription = getDescription();
66 public void handleCommand(ChannelUID channelUID, Command command) {
67 if (command instanceof RefreshType) {
68 updateChannel(channelUID.getId());
73 public void initialize() {
74 logger.debug("Initializing {} Service", serviceDescription);
75 // this is important so FroniusBridgeHandler::childHandlerInitialized gets called
76 Bridge bridge = getBridge();
77 if (bridge == null || bridge.getHandler() == null) {
78 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
79 } else if (bridge.getStatus() == ThingStatus.ONLINE) {
80 updateStatus(ThingStatus.UNKNOWN);
82 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
89 protected void updateChannels() {
90 for (Channel channel : getThing().getChannels()) {
91 updateChannel(channel.getUID().getId());
96 * Update the channel from the last data
98 * @param channelId the id identifying the channel to be updated
100 protected void updateChannel(String channelId) {
101 if (!isLinked(channelId)) {
105 Object value = getValue(channelId);
107 logger.debug("Value retrieved for channel '{}' was null. Can't update.", channelId);
112 if (value instanceof BigDecimal) {
113 state = new DecimalType((BigDecimal) value);
114 } else if (value instanceof Integer) {
115 state = new DecimalType(BigDecimal.valueOf(((Integer) value).longValue()));
116 } else if (value instanceof Double) {
117 state = new DecimalType((double) value);
118 } else if (value instanceof ValueUnit) {
119 state = new DecimalType(((ValueUnit) value).getValue());
120 } else if (value instanceof String) {
121 state = new StringType((String) value);
122 } else if (value instanceof QuantityType) {
123 state = (QuantityType) value;
125 logger.warn("Update channel {}: Unsupported value type {}", channelId, value.getClass().getSimpleName());
127 logger.trace("Update channel {} with state {} ({})", channelId, (state == null) ? "null" : state.toString(),
128 value.getClass().getSimpleName());
130 // Update the channel
132 updateState(channelId, state);
137 * return an internal description for logging
139 * @return the description of the thing
141 protected abstract String getDescription();
144 * get the "new" associated value for a channelId
146 * @param channelId the id identifying the channel
147 * @return the "new" associated value
149 protected abstract Object getValue(String channelId);
152 * Called by the bridge to fetch data and update channels
154 * @param bridgeConfiguration the connected bridge configuration
156 public void refresh(FroniusBridgeConfiguration bridgeConfiguration) {
158 handleRefresh(bridgeConfiguration);
159 if (getThing().getStatus() != ThingStatus.ONLINE) {
160 updateStatus(ThingStatus.ONLINE);
162 } catch (FroniusCommunicationException | RuntimeException e) {
163 logger.debug("Exception caught in refresh() for {}", getThing().getUID().getId(), e);
164 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
169 * This method should be overridden to do whatever a thing must do to update its channels
170 * this function is called from the bridge in a given interval
172 * @param bridgeConfiguration the connected bridge configuration
174 protected abstract void handleRefresh(FroniusBridgeConfiguration bridgeConfiguration)
175 throws FroniusCommunicationException;
179 * @param type response class type
180 * @param url to request
181 * @return the object representation of the json response
183 protected @NonNull <T extends BaseFroniusResponse> T collectDataFromUrl(Class<T> type, String url)
184 throws FroniusCommunicationException {
188 logger.trace("Fetching URL = {}", url);
189 String response = FroniusHttpUtil.executeUrl(url, API_TIMEOUT);
190 logger.trace("aqiResponse = {}", response);
192 T result = gson.fromJson(response, type);
193 if (result == null) {
194 throw new FroniusCommunicationException("Empty json result");
197 HeadStatus status = result.getHead().getStatus();
198 if (status.getCode() == 0) {
202 // Sometimes Fronius would return a HTTP status 200 with a proper JSON data
203 // with Reason: Transfer timeout.
207 // "Reason" : "Transfer timeout.",
208 // "UserMessage" : ""
210 logger.debug("Error from Fronius attempt #{}: {} - {}", attempts, status.getCode(), status.getReason());
212 throw new FroniusCommunicationException(status.getReason());
214 Thread.sleep(500 * attempts);
218 } catch (JsonSyntaxException | NumberFormatException e) {
219 logger.debug("Received Invalid JSON Data", e);
220 throw new FroniusCommunicationException("Invalid JSON data received", e);
221 } catch (InterruptedException e) {
222 Thread.currentThread().interrupt();
223 throw new FroniusCommunicationException("Data collection interrupted", e);