]> git.basschouten.com Git - openhab-addons.git/blob
dd17e7bc27b365ec59820a41ddc6c12fb43a64b9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.solax.internal;
14
15 import java.io.IOException;
16 import java.time.ZonedDateTime;
17 import java.util.HashSet;
18 import java.util.List;
19 import java.util.Set;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.locks.ReentrantLock;
23
24 import javax.measure.Quantity;
25 import javax.measure.Unit;
26
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;
50
51 import com.google.gson.JsonParseException;
52
53 /**
54  * The {@link SolaxLocalAccessHandler} is responsible for handling commands, which are
55  * sent to one of the channels.
56  *
57  * @author Konstantin Polihronov - Initial contribution
58  */
59 @NonNullByDefault
60 public class SolaxLocalAccessHandler extends BaseThingHandler {
61
62     private final Logger logger = LoggerFactory.getLogger(SolaxLocalAccessHandler.class);
63
64     private static final int INITIAL_SCHEDULE_DELAY_SECONDS = 5;
65
66     private @NonNullByDefault({}) LocalHttpConnector localHttpConnector;
67
68     private @Nullable ScheduledFuture<?> schedule;
69
70     private boolean alreadyRemovedUnsupportedChannels;
71
72     private final Set<String> unsupportedExistingChannels = new HashSet<>();
73
74     private final ReentrantLock retrieveDataCallLock = new ReentrantLock();
75
76     public SolaxLocalAccessHandler(Thing thing) {
77         super(thing);
78     }
79
80     @Override
81     public void initialize() {
82         updateStatus(ThingStatus.UNKNOWN);
83
84         SolaxConfiguration config = getConfigAs(SolaxConfiguration.class);
85         localHttpConnector = new LocalHttpConnector(config.password, config.hostname);
86         int refreshInterval = config.refreshInterval;
87         TimeUnit timeUnit = TimeUnit.SECONDS;
88
89         logger.debug("Scheduling regular interval retrieval every {} {}", refreshInterval, timeUnit);
90         schedule = scheduler.scheduleWithFixedDelay(this::retrieveData, INITIAL_SCHEDULE_DELAY_SECONDS, refreshInterval,
91                 timeUnit);
92     }
93
94     private void retrieveData() {
95         if (retrieveDataCallLock.tryLock()) {
96             try {
97                 String rawJsonData = localHttpConnector.retrieveData();
98                 logger.debug("Raw data retrieved = {}", rawJsonData);
99
100                 if (rawJsonData != null && !rawJsonData.isEmpty()) {
101                     updateFromData(rawJsonData);
102                 } else {
103                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
104                             SolaxBindingConstants.I18N_KEY_OFFLINE_COMMUNICATION_ERROR_JSON_CANNOT_BE_RETRIEVED);
105                 }
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());
109             } finally {
110                 retrieveDataCallLock.unlock();
111             }
112         } else {
113             logger.debug("Unable to retrieve data because a request is already in progress.");
114         }
115     }
116
117     private void updateFromData(String rawJsonData) {
118         try {
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;
126                 }
127
128                 InverterData genericInverterData = parser.getData(rawDataBean);
129                 updateChannels(parser, genericInverterData);
130                 updateProperties(genericInverterData);
131
132                 if (getThing().getStatus() != ThingStatus.ONLINE) {
133                     updateStatus(ThingStatus.ONLINE);
134                 }
135             } else {
136                 cancelSchedule();
137                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
138                         "@text/offline.configuration-error.parser-not-implemented [\"" + inverterType.name() + "\"]");
139             }
140         } catch (JsonParseException e) {
141             logger.debug("Unable to deserialize from JSON.", e);
142             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
143         }
144     }
145
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;
150     }
151
152     private InverterType calculateInverterType(LocalConnectRawDataBean rawDataBean) {
153         int type = rawDataBean.getType();
154         return InverterType.fromIndex(type);
155     }
156
157     private void updateProperties(InverterData genericInverterData) {
158         updateProperty(Thing.PROPERTY_SERIAL_NUMBER, genericInverterData.getWifiSerial());
159         updateProperty(SolaxBindingConstants.PROPERTY_INVERTER_TYPE, genericInverterData.getInverterType().name());
160     }
161
162     private void updateChannels(RawDataParser parser, InverterData inverterData) {
163         updateState(SolaxBindingConstants.CHANNEL_RAW_DATA, new StringType(inverterData.getRawData()));
164
165         Set<String> supportedChannels = parser.getSupportedChannels();
166         updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_POWER, inverterData.getPV1Power(), Units.WATT,
167                 supportedChannels);
168         updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_CURRENT, inverterData.getPV1Current(), Units.AMPERE,
169                 supportedChannels);
170         updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV1_VOLTAGE, inverterData.getPV1Voltage(), Units.VOLT,
171                 supportedChannels);
172
173         updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_POWER, inverterData.getPV2Power(), Units.WATT,
174                 supportedChannels);
175         updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_CURRENT, inverterData.getPV2Current(), Units.AMPERE,
176                 supportedChannels);
177         updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV2_VOLTAGE, inverterData.getPV2Voltage(), Units.VOLT,
178                 supportedChannels);
179
180         updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV_TOTAL_POWER, inverterData.getPVTotalPower(), Units.WATT,
181                 supportedChannels);
182         updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_PV_TOTAL_CURRENT, inverterData.getPVTotalCurrent(),
183                 Units.AMPERE, supportedChannels);
184
185         updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_POWER, inverterData.getBatteryPower(), Units.WATT,
186                 supportedChannels);
187         updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_CURRENT, inverterData.getBatteryCurrent(), Units.AMPERE,
188                 supportedChannels);
189         updateChannel(SolaxBindingConstants.CHANNEL_BATTERY_VOLTAGE, inverterData.getBatteryVoltage(), Units.VOLT,
190                 supportedChannels);
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,
200                 supportedChannels);
201         updateChannel(SolaxBindingConstants.CHANNEL_POWER_USAGE, inverterData.getPowerUsage(), Units.WATT,
202                 supportedChannels);
203         updateState(SolaxBindingConstants.CHANNEL_INVERTER_WORKMODE,
204                 new StringType(inverterData.getInverterWorkMode()));
205
206         // Totals
207         updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_ENERGY, inverterData.getTotalEnergy(), Units.KILOWATT_HOUR,
208                 supportedChannels);
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);
219
220         // Today's
221         updateChannel(SolaxBindingConstants.CHANNEL_TODAY_ENERGY, inverterData.getTodayEnergy(), Units.KILOWATT_HOUR,
222                 supportedChannels);
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);
231
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);
241
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);
251
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);
258
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);
265
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);
272
273         // Binding provided data
274         updateState(SolaxBindingConstants.CHANNEL_TIMESTAMP, new DateTimeType(ZonedDateTime.now()));
275     }
276
277     private void removeUnsupportedChannels(Set<String> supportedChannels) {
278         if (supportedChannels.isEmpty()) {
279             return;
280         }
281         List<Channel> channels = getThing().getChannels();
282         List<Channel> channelsToRemove = channels.stream()
283                 .filter(channel -> !supportedChannels.contains(channel.getUID().getId())).toList();
284
285         if (!channelsToRemove.isEmpty()) {
286             if (logger.isDebugEnabled()) {
287                 logRemovedChannels(channelsToRemove);
288             }
289             updateThing(editThing().withoutChannels(channelsToRemove).build());
290         }
291     }
292
293     private void logRemovedChannels(List<Channel> channelsToRemove) {
294         List<String> channelsToRemoveForLog = channelsToRemove.stream().map(channel -> channel.getUID().getId())
295                 .toList();
296         logger.debug("Detected unsupported channels for the current inverter. Channels to be removed: {}",
297                 channelsToRemoveForLog);
298     }
299
300     @Override
301     public void handleCommand(ChannelUID channelUID, Command command) {
302         if (command instanceof RefreshType) {
303             scheduler.execute(this::retrieveData);
304         } else {
305             logger.debug("Binding {} only supports refresh command", SolaxBindingConstants.BINDING_ID);
306         }
307     }
308
309     @Override
310     public void dispose() {
311         super.dispose();
312         cancelSchedule();
313     }
314
315     private void cancelSchedule() {
316         ScheduledFuture<?> schedule = this.schedule;
317         if (schedule != null) {
318             schedule.cancel(true);
319             this.schedule = null;
320         }
321     }
322
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);
331                 logger.warn(
332                         "Channel {} is marked as supported, but its value is out of the defined range. Value = {}. This is unexpected behaviour. Please file a bug.",
333                         channelID, value);
334             }
335         }
336     }
337 }