]> git.basschouten.com Git - openhab-addons.git/blob
c08ede75b44055f8e904a60f380e4841e72f43a6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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<String>();
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_BATTERY_STATE_OF_CHARGE, inverterData.getBatteryLevel(),
194                 Units.PERCENT, supportedChannels);
195         updateChannel(SolaxBindingConstants.CHANNEL_FEED_IN_POWER, inverterData.getFeedInPower(), Units.WATT,
196                 supportedChannels);
197         updateChannel(SolaxBindingConstants.CHANNEL_POWER_USAGE, inverterData.getPowerUsage(), Units.WATT,
198                 supportedChannels);
199
200         // Totals
201         updateChannel(SolaxBindingConstants.CHANNEL_TOTAL_ENERGY, inverterData.getTotalEnergy(), Units.KILOWATT_HOUR,
202                 supportedChannels);
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);
213
214         // Today's
215         updateChannel(SolaxBindingConstants.CHANNEL_TODAY_ENERGY, inverterData.getTodayEnergy(), Units.KILOWATT_HOUR,
216                 supportedChannels);
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);
225
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);
235
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);
245
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);
252
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);
259
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);
266
267         // Binding provided data
268         updateState(SolaxBindingConstants.CHANNEL_TIMESTAMP, new DateTimeType(ZonedDateTime.now()));
269     }
270
271     private void removeUnsupportedChannels(Set<String> supportedChannels) {
272         if (supportedChannels.isEmpty()) {
273             return;
274         }
275         List<Channel> channels = getThing().getChannels();
276         List<Channel> channelsToRemove = channels.stream()
277                 .filter(channel -> !supportedChannels.contains(channel.getUID().getId())).toList();
278
279         if (!channelsToRemove.isEmpty()) {
280             if (logger.isDebugEnabled()) {
281                 logRemovedChannels(channelsToRemove);
282             }
283             updateThing(editThing().withoutChannels(channelsToRemove).build());
284         }
285     }
286
287     private void logRemovedChannels(List<Channel> channelsToRemove) {
288         List<String> channelsToRemoveForLog = channelsToRemove.stream().map(channel -> channel.getUID().getId())
289                 .toList();
290         logger.debug("Detected unsupported channels for the current inverter. Channels to be removed: {}",
291                 channelsToRemoveForLog);
292     }
293
294     @Override
295     public void handleCommand(ChannelUID channelUID, Command command) {
296         if (command instanceof RefreshType) {
297             scheduler.execute(this::retrieveData);
298         } else {
299             logger.debug("Binding {} only supports refresh command", SolaxBindingConstants.BINDING_ID);
300         }
301     }
302
303     @Override
304     public void dispose() {
305         super.dispose();
306         cancelSchedule();
307     }
308
309     private void cancelSchedule() {
310         ScheduledFuture<?> schedule = this.schedule;
311         if (schedule != null) {
312             schedule.cancel(true);
313             this.schedule = null;
314         }
315     }
316
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);
325                 logger.warn(
326                         "Channel {} is marked as supported, but its value is out of the defined range. Value = {}. This is unexpected behaviour. Please file a bug.",
327                         channelID, value);
328             }
329         }
330     }
331 }