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.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;
23 import javax.measure.Quantity;
24 import javax.measure.Unit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.solax.internal.connectivity.LocalHttpConnector;
29 import org.openhab.binding.solax.internal.connectivity.rawdata.LocalConnectRawDataBean;
30 import org.openhab.binding.solax.internal.model.InverterData;
31 import org.openhab.binding.solax.internal.model.InverterType;
32 import org.openhab.binding.solax.internal.model.parsers.RawDataParser;
33 import org.openhab.core.library.types.DateTimeType;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.library.unit.SIUnits;
37 import org.openhab.core.library.unit.Units;
38 import org.openhab.core.thing.Channel;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.UnDefType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import com.google.gson.JsonParseException;
52 * The {@link SolaxLocalAccessHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Konstantin Polihronov - Initial contribution
58 public class SolaxLocalAccessHandler extends BaseThingHandler {
60 private final Logger logger = LoggerFactory.getLogger(SolaxLocalAccessHandler.class);
62 private static final int INITIAL_SCHEDULE_DELAY_SECONDS = 5;
64 private @NonNullByDefault({}) LocalHttpConnector localHttpConnector;
66 private @Nullable ScheduledFuture<?> schedule;
68 private boolean alreadyRemovedUnsupportedChannels;
70 private final Set<String> unsupportedExistingChannels = new HashSet<String>();
72 public SolaxLocalAccessHandler(Thing thing) {
77 public void initialize() {
78 updateStatus(ThingStatus.UNKNOWN);
80 SolaxConfiguration config = getConfigAs(SolaxConfiguration.class);
81 localHttpConnector = new LocalHttpConnector(config.password, config.hostname);
82 int refreshInterval = config.refreshInterval;
83 TimeUnit timeUnit = TimeUnit.SECONDS;
85 logger.debug("Scheduling regular interval retrieval every {} {}", refreshInterval, timeUnit);
86 schedule = scheduler.scheduleWithFixedDelay(this::retrieveData, INITIAL_SCHEDULE_DELAY_SECONDS, refreshInterval,
90 private void retrieveData() {
92 String rawJsonData = localHttpConnector.retrieveData();
93 logger.debug("Raw data retrieved = {}", rawJsonData);
95 if (rawJsonData != null && !rawJsonData.isEmpty()) {
96 updateFromData(rawJsonData);
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
99 SolaxBindingConstants.I18N_KEY_OFFLINE_COMMUNICATION_ERROR_JSON_CANNOT_BE_RETRIEVED);
101 } catch (IOException e) {
102 logger.debug("Exception received while attempting to retrieve data via HTTP", e);
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
107 private void updateFromData(String rawJsonData) {
109 LocalConnectRawDataBean rawDataBean = parseJson(rawJsonData);
110 InverterType inverterType = calculateInverterType(rawDataBean);
111 RawDataParser parser = inverterType.getParser();
112 if (parser != null) {
113 if (!alreadyRemovedUnsupportedChannels) {
114 removeUnsupportedChannels(inverterType.getSupportedChannels());
115 alreadyRemovedUnsupportedChannels = true;
118 InverterData genericInverterData = parser.getData(rawDataBean);
119 updateChannels(parser, genericInverterData);
120 updateProperties(genericInverterData);
122 if (getThing().getStatus() != ThingStatus.ONLINE) {
123 updateStatus(ThingStatus.ONLINE);
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
128 "@text/offline.configuration-error.parser-not-implemented [\"" + inverterType.name() + "\"]");
130 } catch (JsonParseException e) {
131 logger.debug("Unable to deserialize from JSON.", e);
132 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
136 private LocalConnectRawDataBean parseJson(String rawJsonData) {
137 LocalConnectRawDataBean inverterParsedData = LocalConnectRawDataBean.fromJson(rawJsonData);
138 logger.debug("Received a new inverter JSON object. Data = {}", inverterParsedData.toString());
139 return inverterParsedData;
142 private InverterType calculateInverterType(LocalConnectRawDataBean rawDataBean) {
143 int type = rawDataBean.getType();
144 return InverterType.fromIndex(type);
147 private void updateProperties(InverterData genericInverterData) {
148 updateProperty(Thing.PROPERTY_SERIAL_NUMBER, genericInverterData.getWifiSerial());
149 updateProperty(SolaxBindingConstants.PROPERTY_INVERTER_TYPE, genericInverterData.getInverterType().name());
152 private void updateChannels(RawDataParser parser, InverterData inverterData) {
153 updateState(SolaxBindingConstants.CHANNEL_RAW_DATA, new StringType(inverterData.getRawData()));
155 Set<String> supportedChannels = parser.getSupportedChannels();
156 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_POWER, inverterData.getPV1Power(), Units.WATT,
158 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_CURRENT, inverterData.getPV1Current(), Units.AMPERE,
160 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_VOLTAGE, inverterData.getPV1Voltage(), Units.VOLT,
163 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_POWER, inverterData.getPV2Power(), Units.WATT,
165 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_CURRENT, inverterData.getPV2Current(), Units.AMPERE,
167 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_VOLTAGE, inverterData.getPV2Voltage(), Units.VOLT,
170 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV_TOTAL_POWER, inverterData.getPVTotalPower(), Units.WATT,
172 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV_TOTAL_CURRENT, inverterData.getPVTotalCurrent(),
173 Units.AMPERE, supportedChannels);
175 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_POWER, inverterData.getBatteryPower(), Units.WATT,
177 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_CURRENT, inverterData.getBatteryCurrent(), Units.AMPERE,
179 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_VOLTAGE, inverterData.getBatteryVoltage(), Units.VOLT,
181 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_TEMPERATURE, inverterData.getBatteryTemperature(),
182 SIUnits.CELSIUS, supportedChannels);
183 updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_STATE_OF_CHARGE, inverterData.getBatteryLevel(),
184 Units.PERCENT, supportedChannels);
185 updateChannel(SolaxBindingConstants.CHANNEL_FEED_IN_POWER, inverterData.getFeedInPower(), Units.WATT,
187 updateChannel(SolaxBindingConstants.CHANNEL_POWER_USAGE, inverterData.getPowerUsage(), Units.WATT,
191 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_ENERGY, inverterData.getTotalEnergy(), Units.KILOWATT_HOUR,
193 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_BATTERY_DISCHARGE_ENERGY,
194 inverterData.getTotalBatteryDischargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
195 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_BATTERY_CHARGE_ENERGY,
196 inverterData.getTotalBatteryChargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
197 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_PV_ENERGY, inverterData.getTotalPVEnergy(),
198 Units.KILOWATT_HOUR, supportedChannels);
199 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_FEED_IN_ENERGY, inverterData.getTotalFeedInEnergy(),
200 Units.KILOWATT_HOUR, supportedChannels);
201 updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_CONSUMPTION, inverterData.getTotalConsumption(),
202 Units.KILOWATT_HOUR, supportedChannels);
205 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_ENERGY, inverterData.getTodayEnergy(), Units.KILOWATT_HOUR,
207 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_BATTERY_DISCHARGE_ENERGY,
208 inverterData.getTodayBatteryDischargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
209 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_BATTERY_CHARGE_ENERGY,
210 inverterData.getTodayBatteryChargeEnergy(), Units.KILOWATT_HOUR, supportedChannels);
211 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_FEED_IN_ENERGY, inverterData.getTodayFeedInEnergy(),
212 Units.KILOWATT_HOUR, supportedChannels);
213 updateChannel(SolaxBindingConstants.CHANNEL_TODAY_CONSUMPTION, inverterData.getTodayConsumption(),
214 Units.KILOWATT_HOUR, supportedChannels);
216 // Single phase specific channels
217 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER, inverterData.getInverterOutputPower(),
218 Units.WATT, supportedChannels);
219 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT, inverterData.getInverterCurrent(),
220 Units.AMPERE, supportedChannels);
221 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE, inverterData.getInverterVoltage(),
222 Units.VOLT, supportedChannels);
223 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY, inverterData.getInverterFrequency(),
224 Units.HERTZ, supportedChannels);
226 // Three phase specific channels
227 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE1, inverterData.getOutputPowerPhase1(),
228 Units.WATT, supportedChannels);
229 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE2, inverterData.getOutputPowerPhase2(),
230 Units.WATT, supportedChannels);
231 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_POWER_PHASE3, inverterData.getOutputPowerPhase3(),
232 Units.WATT, supportedChannels);
233 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_TOTAL_OUTPUT_POWER, inverterData.getTotalOutputPower(),
234 Units.WATT, supportedChannels);
236 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE1, inverterData.getCurrentPhase1(),
237 Units.AMPERE, supportedChannels);
238 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE2, inverterData.getCurrentPhase2(),
239 Units.AMPERE, supportedChannels);
240 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_CURRENT_PHASE3, inverterData.getCurrentPhase3(),
241 Units.AMPERE, supportedChannels);
243 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE1, inverterData.getVoltagePhase1(),
244 Units.VOLT, supportedChannels);
245 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE2, inverterData.getVoltagePhase2(),
246 Units.VOLT, supportedChannels);
247 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_VOLTAGE_PHASE3, inverterData.getVoltagePhase3(),
248 Units.VOLT, supportedChannels);
250 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE1, inverterData.getFrequencyPhase1(),
251 Units.HERTZ, supportedChannels);
252 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE2, inverterData.getFrequencyPhase2(),
253 Units.HERTZ, supportedChannels);
254 updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE3, inverterData.getFrequencyPhase3(),
255 Units.HERTZ, supportedChannels);
257 // Binding provided data
258 updateState(SolaxBindingConstants.CHANNEL_TIMESTAMP, new DateTimeType(ZonedDateTime.now()));
261 private void removeUnsupportedChannels(Set<String> supportedChannels) {
262 if (supportedChannels.isEmpty()) {
265 List<Channel> channels = getThing().getChannels();
266 List<Channel> channelsToRemove = channels.stream()
267 .filter(channel -> !supportedChannels.contains(channel.getUID().getId())).toList();
269 if (!channelsToRemove.isEmpty()) {
270 if (logger.isDebugEnabled()) {
271 logRemovedChannels(channelsToRemove);
273 updateThing(editThing().withoutChannels(channelsToRemove).build());
277 private void logRemovedChannels(List<Channel> channelsToRemove) {
278 List<String> channelsToRemoveForLog = channelsToRemove.stream().map(channel -> channel.getUID().getId())
280 logger.debug("Detected unsupported channels for the current inverter. Channels to be removed: {}",
281 channelsToRemoveForLog);
285 public void handleCommand(ChannelUID channelUID, Command command) {
286 // Nothing to do here as of now. Maybe implement a REFRESH command in the future.
290 public void dispose() {
295 private void cancelSchedule() {
296 ScheduledFuture<?> schedule = this.schedule;
297 if (schedule != null) {
298 schedule.cancel(true);
299 this.schedule = null;
303 private <T extends Quantity<T>> void updateChannel(String channelID, double value, Unit<T> unit,
304 Set<String> supportedChannels) {
305 if (supportedChannels.contains(channelID)) {
306 if (value > Short.MIN_VALUE) {
307 updateState(channelID, new QuantityType<>(value, unit));
308 } else if (!unsupportedExistingChannels.contains(channelID)) {
309 updateState(channelID, UnDefType.UNDEF);
310 unsupportedExistingChannels.add(channelID);
312 "Channel {} is marked as supported, but its value is out of the defined range. Value = {}. This is unexpected behaviour. Please file a bug.",