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<>();
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_INVERTER_TEMPERATURE1, inverterData.getInverterTemperature1(),
194 SIUnits.CELSIUS, supportedChannels);
195 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_TEMPERATURE2, inverterData.getInverterTemperature2(),
196 SIUnits.CELSIUS, supportedChannels);
197 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_STATE_OF_CHARGE, inverterData.getBatteryLevel(),
198 Units.PERCENT, supportedChannels);
199 updateChannel(SolaxBindingConstants.CHANNEL_FEED_IN_POWER, inverterData.getFeedInPower(), Units.WATT,
201 updateChannel(SolaxBindingConstants.CHANNEL_POWER_USAGE, inverterData.getPowerUsage(), Units.WATT,
203 updateState(SolaxBindingConstants.CHANNEL_INVERTER_WORKMODE,
204 new StringType(inverterData.getInverterWorkMode()));
207 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_ENERGY, inverterData.getTotalEnergy(), Units.KILOWATT_HOUR,
209 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_BATTERY_DISCHARGE_ENERGY,
210 inverterData.getTotalBatteryDischargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
211 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_BATTERY_CHARGE_ENERGY,
212 inverterData.getTotalBatteryChargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
213 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_PV_ENERGY, inverterData.getTotalPVEnergy(),
214 Units.KILOWATT_HOUR, supportedChannels);
215 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_FEED_IN_ENERGY, inverterData.getTotalFeedInEnergy(),
216 Units.KILOWATT_HOUR, supportedChannels);
217 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_CONSUMPTION, inverterData.getTotalConsumption(),
218 Units.KILOWATT_HOUR, supportedChannels);
221 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_ENERGY, inverterData.getTodayEnergy(), Units.KILOWATT_HOUR,
223 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_BATTERY_DISCHARGE_ENERGY,
224 inverterData.getTodayBatteryDischargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
225 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_BATTERY_CHARGE_ENERGY,
226 inverterData.getTodayBatteryChargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
227 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_FEED_IN_ENERGY, inverterData.getTodayFeedInEnergy(),
228 Units.KILOWATT_HOUR, supportedChannels);
229 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_CONSUMPTION, inverterData.getTodayConsumption(),
230 Units.KILOWATT_HOUR, supportedChannels);
232 // Single phase specific channels
233 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER, inverterData.getInverterOutputPower(),
234 Units.WATT, supportedChannels);
235 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT, inverterData.getInverterCurrent(),
236 Units.AMPERE, supportedChannels);
237 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE, inverterData.getInverterVoltage(),
238 Units.VOLT, supportedChannels);
239 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY, inverterData.getInverterFrequency(),
240 Units.HERTZ, supportedChannels);
242 // Three phase specific channels
243 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE1, inverterData.getOutputPowerPhase1(),
244 Units.WATT, supportedChannels);
245 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE2, inverterData.getOutputPowerPhase2(),
246 Units.WATT, supportedChannels);
247 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE3, inverterData.getOutputPowerPhase3(),
248 Units.WATT, supportedChannels);
249 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_TOTAL_OUTPUT_POWER, inverterData.getTotalOutputPower(),
250 Units.WATT, supportedChannels);
252 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE1, inverterData.getCurrentPhase1(),
253 Units.AMPERE, supportedChannels);
254 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE2, inverterData.getCurrentPhase2(),
255 Units.AMPERE, supportedChannels);
256 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE3, inverterData.getCurrentPhase3(),
257 Units.AMPERE, supportedChannels);
259 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE1, inverterData.getVoltagePhase1(),
260 Units.VOLT, supportedChannels);
261 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE2, inverterData.getVoltagePhase2(),
262 Units.VOLT, supportedChannels);
263 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE3, inverterData.getVoltagePhase3(),
264 Units.VOLT, supportedChannels);
266 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE1, inverterData.getFrequencyPhase1(),
267 Units.HERTZ, supportedChannels);
268 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE2, inverterData.getFrequencyPhase2(),
269 Units.HERTZ, supportedChannels);
270 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE3, inverterData.getFrequencyPhase3(),
271 Units.HERTZ, supportedChannels);
273 // Binding provided data
274 updateState(SolaxBindingConstants.CHANNEL_TIMESTAMP, new DateTimeType(ZonedDateTime.now()));
277 private void removeUnsupportedChannels(Set<String> supportedChannels) {
278 if (supportedChannels.isEmpty()) {
281 List<Channel> channels = getThing().getChannels();
282 List<Channel> channelsToRemove = channels.stream()
283 .filter(channel -> !supportedChannels.contains(channel.getUID().getId())).toList();
285 if (!channelsToRemove.isEmpty()) {
286 if (logger.isDebugEnabled()) {
287 logRemovedChannels(channelsToRemove);
289 updateThing(editThing().withoutChannels(channelsToRemove).build());
293 private void logRemovedChannels(List<Channel> channelsToRemove) {
294 List<String> channelsToRemoveForLog = channelsToRemove.stream().map(channel -> channel.getUID().getId())
296 logger.debug("Detected unsupported channels for the current inverter. Channels to be removed: {}",
297 channelsToRemoveForLog);
301 public void handleCommand(ChannelUID channelUID, Command command) {
302 if (command instanceof RefreshType) {
303 scheduler.execute(this::retrieveData);
305 logger.debug("Binding {} only supports refresh command", SolaxBindingConstants.BINDING_ID);
310 public void dispose() {
315 private void cancelSchedule() {
316 ScheduledFuture<?> schedule = this.schedule;
317 if (schedule != null) {
318 schedule.cancel(true);
319 this.schedule = null;
323 private <T extends Quantity<T>> void updateChannel(String channelID, double value, Unit<T> unit,
324 Set<String> supportedChannels) {
325 if (supportedChannels.contains(channelID)) {
326 if (value > Short.MIN_VALUE) {
327 updateState(channelID, new QuantityType<>(value, unit));
328 } else if (!unsupportedExistingChannels.contains(channelID)) {
329 updateState(channelID, UnDefType.UNDEF);
330 unsupportedExistingChannels.add(channelID);
332 "Channel {} is marked as supported, but its value is out of the defined range. Value = {}. This is unexpected behaviour. Please file a bug.",