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.solax.internal;
15 import java.io.IOException;
16 import java.time.ZonedDateTime;
17 import java.util.HashSet;
18 import java.util.List;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.locks.ReentrantLock;
24 import javax.measure.Quantity;
25 import javax.measure.Unit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.solax.internal.connectivity.LocalHttpConnector;
30 import org.openhab.binding.solax.internal.connectivity.rawdata.LocalConnectRawDataBean;
31 import org.openhab.binding.solax.internal.model.InverterData;
32 import org.openhab.binding.solax.internal.model.InverterType;
33 import org.openhab.binding.solax.internal.model.parsers.RawDataParser;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.library.unit.SIUnits;
38 import org.openhab.core.library.unit.Units;
39 import org.openhab.core.thing.Channel;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
51 import com.google.gson.JsonParseException;
54 * The {@link SolaxLocalAccessHandler} is responsible for handling commands, which are
55 * sent to one of the channels.
57 * @author Konstantin Polihronov - Initial contribution
60 public class SolaxLocalAccessHandler extends BaseThingHandler {
62 private final Logger logger = LoggerFactory.getLogger(SolaxLocalAccessHandler.class);
64 private static final int INITIAL_SCHEDULE_DELAY_SECONDS = 5;
66 private @NonNullByDefault({}) LocalHttpConnector localHttpConnector;
68 private @Nullable ScheduledFuture<?> schedule;
70 private boolean alreadyRemovedUnsupportedChannels;
72 private final Set<String> unsupportedExistingChannels = new HashSet<String>();
74 private final ReentrantLock retrieveDataCallLock = new ReentrantLock();
76 public SolaxLocalAccessHandler(Thing thing) {
81 public void initialize() {
82 updateStatus(ThingStatus.UNKNOWN);
84 SolaxConfiguration config = getConfigAs(SolaxConfiguration.class);
85 localHttpConnector = new LocalHttpConnector(config.password, config.hostname);
86 int refreshInterval = config.refreshInterval;
87 TimeUnit timeUnit = TimeUnit.SECONDS;
89 logger.debug("Scheduling regular interval retrieval every {} {}", refreshInterval, timeUnit);
90 schedule = scheduler.scheduleWithFixedDelay(this::retrieveData, INITIAL_SCHEDULE_DELAY_SECONDS, refreshInterval,
94 private void retrieveData() {
95 if (retrieveDataCallLock.tryLock()) {
97 String rawJsonData = localHttpConnector.retrieveData();
98 logger.debug("Raw data retrieved = {}", rawJsonData);
100 if (rawJsonData != null && !rawJsonData.isEmpty()) {
101 updateFromData(rawJsonData);
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
104 SolaxBindingConstants.I18N_KEY_OFFLINE_COMMUNICATION_ERROR_JSON_CANNOT_BE_RETRIEVED);
106 } catch (IOException e) {
107 logger.debug("Exception received while attempting to retrieve data via HTTP", e);
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
110 retrieveDataCallLock.unlock();
113 logger.debug("Unable to retrieve data because a request is already in progress.");
117 private void updateFromData(String rawJsonData) {
119 LocalConnectRawDataBean rawDataBean = parseJson(rawJsonData);
120 InverterType inverterType = calculateInverterType(rawDataBean);
121 RawDataParser parser = inverterType.getParser();
122 if (parser != null) {
123 if (!alreadyRemovedUnsupportedChannels) {
124 removeUnsupportedChannels(inverterType.getSupportedChannels());
125 alreadyRemovedUnsupportedChannels = true;
128 InverterData genericInverterData = parser.getData(rawDataBean);
129 updateChannels(parser, genericInverterData);
130 updateProperties(genericInverterData);
132 if (getThing().getStatus() != ThingStatus.ONLINE) {
133 updateStatus(ThingStatus.ONLINE);
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
138 "@text/offline.configuration-error.parser-not-implemented [\"" + inverterType.name() + "\"]");
140 } catch (JsonParseException e) {
141 logger.debug("Unable to deserialize from JSON.", e);
142 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
146 private LocalConnectRawDataBean parseJson(String rawJsonData) {
147 LocalConnectRawDataBean inverterParsedData = LocalConnectRawDataBean.fromJson(rawJsonData);
148 logger.debug("Received a new inverter JSON object. Data = {}", inverterParsedData.toString());
149 return inverterParsedData;
152 private InverterType calculateInverterType(LocalConnectRawDataBean rawDataBean) {
153 int type = rawDataBean.getType();
154 return InverterType.fromIndex(type);
157 private void updateProperties(InverterData genericInverterData) {
158 updateProperty(Thing.PROPERTY_SERIAL_NUMBER, genericInverterData.getWifiSerial());
159 updateProperty(SolaxBindingConstants.PROPERTY_INVERTER_TYPE, genericInverterData.getInverterType().name());
162 private void updateChannels(RawDataParser parser, InverterData inverterData) {
163 updateState(SolaxBindingConstants.CHANNEL_RAW_DATA, new StringType(inverterData.getRawData()));
165 Set<String> supportedChannels = parser.getSupportedChannels();
166 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_POWER, inverterData.getPV1Power(), Units.WATT,
168 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_CURRENT, inverterData.getPV1Current(), Units.AMPERE,
170 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_VOLTAGE, inverterData.getPV1Voltage(), Units.VOLT,
173 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_POWER, inverterData.getPV2Power(), Units.WATT,
175 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_CURRENT, inverterData.getPV2Current(), Units.AMPERE,
177 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_VOLTAGE, inverterData.getPV2Voltage(), Units.VOLT,
180 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV_TOTAL_POWER, inverterData.getPVTotalPower(), Units.WATT,
182 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV_TOTAL_CURRENT, inverterData.getPVTotalCurrent(),
183 Units.AMPERE, supportedChannels);
185 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_POWER, inverterData.getBatteryPower(), Units.WATT,
187 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_CURRENT, inverterData.getBatteryCurrent(), Units.AMPERE,
189 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_VOLTAGE, inverterData.getBatteryVoltage(), Units.VOLT,
191 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_TEMPERATURE, inverterData.getBatteryTemperature(),
192 SIUnits.CELSIUS, supportedChannels);
193 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_STATE_OF_CHARGE, inverterData.getBatteryLevel(),
194 Units.PERCENT, supportedChannels);
195 updateChannel(SolaxBindingConstants.CHANNEL_FEED_IN_POWER, inverterData.getFeedInPower(), Units.WATT,
197 updateChannel(SolaxBindingConstants.CHANNEL_POWER_USAGE, inverterData.getPowerUsage(), Units.WATT,
201 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_ENERGY, inverterData.getTotalEnergy(), Units.KILOWATT_HOUR,
203 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_BATTERY_DISCHARGE_ENERGY,
204 inverterData.getTotalBatteryDischargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
205 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_BATTERY_CHARGE_ENERGY,
206 inverterData.getTotalBatteryChargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
207 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_PV_ENERGY, inverterData.getTotalPVEnergy(),
208 Units.KILOWATT_HOUR, supportedChannels);
209 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_FEED_IN_ENERGY, inverterData.getTotalFeedInEnergy(),
210 Units.KILOWATT_HOUR, supportedChannels);
211 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_CONSUMPTION, inverterData.getTotalConsumption(),
212 Units.KILOWATT_HOUR, supportedChannels);
215 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_ENERGY, inverterData.getTodayEnergy(), Units.KILOWATT_HOUR,
217 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_BATTERY_DISCHARGE_ENERGY,
218 inverterData.getTodayBatteryDischargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
219 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_BATTERY_CHARGE_ENERGY,
220 inverterData.getTodayBatteryChargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
221 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_FEED_IN_ENERGY, inverterData.getTodayFeedInEnergy(),
222 Units.KILOWATT_HOUR, supportedChannels);
223 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_CONSUMPTION, inverterData.getTodayConsumption(),
224 Units.KILOWATT_HOUR, supportedChannels);
226 // Single phase specific channels
227 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER, inverterData.getInverterOutputPower(),
228 Units.WATT, supportedChannels);
229 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT, inverterData.getInverterCurrent(),
230 Units.AMPERE, supportedChannels);
231 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE, inverterData.getInverterVoltage(),
232 Units.VOLT, supportedChannels);
233 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY, inverterData.getInverterFrequency(),
234 Units.HERTZ, supportedChannels);
236 // Three phase specific channels
237 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE1, inverterData.getOutputPowerPhase1(),
238 Units.WATT, supportedChannels);
239 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE2, inverterData.getOutputPowerPhase2(),
240 Units.WATT, supportedChannels);
241 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE3, inverterData.getOutputPowerPhase3(),
242 Units.WATT, supportedChannels);
243 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_TOTAL_OUTPUT_POWER, inverterData.getTotalOutputPower(),
244 Units.WATT, supportedChannels);
246 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE1, inverterData.getCurrentPhase1(),
247 Units.AMPERE, supportedChannels);
248 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE2, inverterData.getCurrentPhase2(),
249 Units.AMPERE, supportedChannels);
250 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE3, inverterData.getCurrentPhase3(),
251 Units.AMPERE, supportedChannels);
253 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE1, inverterData.getVoltagePhase1(),
254 Units.VOLT, supportedChannels);
255 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE2, inverterData.getVoltagePhase2(),
256 Units.VOLT, supportedChannels);
257 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE3, inverterData.getVoltagePhase3(),
258 Units.VOLT, supportedChannels);
260 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE1, inverterData.getFrequencyPhase1(),
261 Units.HERTZ, supportedChannels);
262 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE2, inverterData.getFrequencyPhase2(),
263 Units.HERTZ, supportedChannels);
264 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE3, inverterData.getFrequencyPhase3(),
265 Units.HERTZ, supportedChannels);
267 // Binding provided data
268 updateState(SolaxBindingConstants.CHANNEL_TIMESTAMP, new DateTimeType(ZonedDateTime.now()));
271 private void removeUnsupportedChannels(Set<String> supportedChannels) {
272 if (supportedChannels.isEmpty()) {
275 List<Channel> channels = getThing().getChannels();
276 List<Channel> channelsToRemove = channels.stream()
277 .filter(channel -> !supportedChannels.contains(channel.getUID().getId())).toList();
279 if (!channelsToRemove.isEmpty()) {
280 if (logger.isDebugEnabled()) {
281 logRemovedChannels(channelsToRemove);
283 updateThing(editThing().withoutChannels(channelsToRemove).build());
287 private void logRemovedChannels(List<Channel> channelsToRemove) {
288 List<String> channelsToRemoveForLog = channelsToRemove.stream().map(channel -> channel.getUID().getId())
290 logger.debug("Detected unsupported channels for the current inverter. Channels to be removed: {}",
291 channelsToRemoveForLog);
295 public void handleCommand(ChannelUID channelUID, Command command) {
296 if (command instanceof RefreshType) {
297 scheduler.execute(this::retrieveData);
299 logger.debug("Binding {} only supports refresh command", SolaxBindingConstants.BINDING_ID);
304 public void dispose() {
309 private void cancelSchedule() {
310 ScheduledFuture<?> schedule = this.schedule;
311 if (schedule != null) {
312 schedule.cancel(true);
313 this.schedule = null;
317 private <T extends Quantity<T>> void updateChannel(String channelID, double value, Unit<T> unit,
318 Set<String> supportedChannels) {
319 if (supportedChannels.contains(channelID)) {
320 if (value > Short.MIN_VALUE) {
321 updateState(channelID, new QuantityType<>(value, unit));
322 } else if (!unsupportedExistingChannels.contains(channelID)) {
323 updateState(channelID, UnDefType.UNDEF);
324 unsupportedExistingChannels.add(channelID);
326 "Channel {} is marked as supported, but its value is out of the defined range. Value = {}. This is unexpected behaviour. Please file a bug.",