]> git.basschouten.com Git - openhab-addons.git/blob
756e27711bce1d9613bc96bfb0ee7db644e13b35
[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.solarwatt.internal.handler;
14
15 import static org.openhab.binding.solarwatt.internal.SolarwattBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.time.DateTimeException;
19 import java.time.Duration;
20 import java.time.Instant;
21 import java.time.ZoneId;
22 import java.time.ZonedDateTime;
23 import java.time.temporal.ChronoUnit;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.openhab.binding.solarwatt.internal.channel.SolarwattChannelTypeProvider;
35 import org.openhab.binding.solarwatt.internal.configuration.SolarwattBridgeConfiguration;
36 import org.openhab.binding.solarwatt.internal.configuration.SolarwattThingConfiguration;
37 import org.openhab.binding.solarwatt.internal.discovery.SolarwattDevicesDiscoveryService;
38 import org.openhab.binding.solarwatt.internal.domain.SolarwattChannel;
39 import org.openhab.binding.solarwatt.internal.domain.model.Device;
40 import org.openhab.binding.solarwatt.internal.domain.model.EnergyManager;
41 import org.openhab.binding.solarwatt.internal.exception.SolarwattConnectionException;
42 import org.openhab.core.cache.ExpiringCache;
43 import org.openhab.core.library.types.DateTimeType;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.thing.binding.ThingHandler;
50 import org.openhab.core.thing.binding.ThingHandlerService;
51 import org.openhab.core.thing.binding.builder.ThingBuilder;
52 import org.openhab.core.thing.type.ChannelTypeUID;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.RefreshType;
55 import org.openhab.core.types.State;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 /**
60  * The {@link EnergyManagerHandler} is responsible for handling energy manager thing itself
61  * and handle data retrieval for the child things.
62  *
63  * @author Sven Carstens - Initial contribution
64  */
65 @NonNullByDefault
66 public class EnergyManagerHandler extends BaseBridgeHandler {
67
68     private final Logger logger = LoggerFactory.getLogger(EnergyManagerHandler.class);
69
70     private final EnergyManagerConnector connector;
71     private final SolarwattChannelTypeProvider channelTypeProvider;
72
73     private @Nullable ExpiringCache<Map<String, Device>> devicesCache;
74     private @Nullable ScheduledFuture<?> refreshJob;
75
76     private @Nullable ZoneId zoneId;
77
78     /**
79      * Guid of this energy manager itself.
80      */
81     private @Nullable String energyManagerGuid;
82
83     /**
84      * Runner for the {@link ExpiringCache} refresh.
85      *
86      * Triggers update of all child things.
87      */
88     private final Runnable refreshRunnable = () -> {
89         EnergyManagerHandler.this.updateChannels();
90         EnergyManagerHandler.this.updateAllChildThings();
91     };
92
93     /**
94      * Create the handler.
95      *
96      * @param thing for which the handler is responsible
97      * @param channelTypeProvider provider for the channels
98      * @param httpClient connect to energy manager via this client
99      */
100     public EnergyManagerHandler(final Bridge thing, final SolarwattChannelTypeProvider channelTypeProvider,
101             final HttpClient httpClient) {
102         super(thing);
103         this.connector = new EnergyManagerConnector(httpClient);
104         this.channelTypeProvider = channelTypeProvider;
105     }
106
107     /**
108      * Get services which are provided by this handler.
109      *
110      * Only service discovery is provided by
111      * 
112      * @return collection containing our discovery service
113      */
114     @Override
115     public Collection<Class<? extends ThingHandlerService>> getServices() {
116         return Set.of(SolarwattDevicesDiscoveryService.class);
117     }
118
119     /**
120      * Execute the desired commands.
121      *
122      * Only refresh is supported and relayed to all childs of this thing.
123      *
124      * @param channelUID for which the command is issued
125      * @param command command issued
126      */
127     @Override
128     public void handleCommand(ChannelUID channelUID, Command command) {
129         if (command instanceof RefreshType) {
130             this.updateChannels();
131         }
132     }
133
134     /**
135      * Dynnamically updates all known channel states of the energy manager.
136      */
137     public void updateChannels() {
138         Map<String, Device> devices = this.getDevices();
139         if (devices != null) {
140             if (this.energyManagerGuid == null) {
141                 try {
142                     this.findEnergyManagerGuid(devices);
143                 } catch (SolarwattConnectionException ex) {
144                     this.logger.warn("Failed updating EnergyManager channels: {}", ex.getMessage());
145                 }
146             }
147             EnergyManager energyManager = (EnergyManager) devices.get(this.energyManagerGuid);
148
149             if (energyManager != null) {
150                 this.calculateUpdates(energyManager);
151
152                 energyManager.getStateValues().forEach((stateName, stateValue) -> {
153                     this.updateState(stateName, stateValue);
154                 });
155             } else {
156                 this.logger.warn("updateChannels failed, missing device EnergyManager {}", this.energyManagerGuid);
157             }
158         }
159     }
160
161     private void calculateUpdates(EnergyManager energyManager) {
162         State timezoneState = energyManager.getState(CHANNEL_IDTIMEZONE.getChannelName());
163         if (timezoneState != null) {
164             this.zoneId = ZoneId.of(timezoneState.toFullString());
165         }
166
167         BigDecimal timestamp = energyManager.getBigDecimalFromChannel(CHANNEL_TIMESTAMP.getChannelName());
168         if (timestamp.compareTo(BigDecimal.ONE) > 0) {
169             energyManager.addState(CHANNEL_DATETIME.getChannelName(),
170                     new DateTimeType(this.getFromMilliTimestamp(timestamp)));
171         }
172     }
173
174     /**
175      * Initial setup of the channels available for this thing.
176      *
177      * @param device which provides the channels
178      */
179     protected void initDeviceChannels(Device device) {
180         this.assertChannel(new SolarwattChannel(CHANNEL_DATETIME.getChannelName(), "time"));
181
182         device.getSolarwattChannelSet().forEach((channelTag, solarwattChannel) -> {
183             this.assertChannel(solarwattChannel);
184         });
185     }
186
187     /**
188      * Assert that all channels inside of our thing are well defined.
189      *
190      * Only channel which can not be found are created.
191      *
192      * @param solarwattChannel channel description with name and unit
193      */
194     protected void assertChannel(SolarwattChannel solarwattChannel) {
195         ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), solarwattChannel.getChannelName());
196         ChannelTypeUID channelType = this.channelTypeProvider.assertChannelType(solarwattChannel);
197         if (this.getThing().getChannel(channelUID) == null) {
198             ThingBuilder thingBuilder = this.editThing();
199             thingBuilder.withChannel(
200                     SimpleDeviceHandler.getChannelBuilder(solarwattChannel, channelUID, channelType).build());
201
202             this.updateThing(thingBuilder.build());
203         }
204     }
205
206     /**
207      * Finds the guid of the energy manager inside of the known devices.
208      *
209      * @param devices list with known devices
210      * @throws SolarwattConnectionException if there is no energy manager available
211      */
212     private void findEnergyManagerGuid(Map<String, Device> devices) throws SolarwattConnectionException {
213         devices.forEach((guid, device) -> {
214             if (device instanceof EnergyManager) {
215                 this.energyManagerGuid = guid;
216             }
217         });
218
219         if (this.energyManagerGuid == null) {
220             throw new SolarwattConnectionException("unable to find energy manager");
221         }
222     }
223
224     /**
225      * Setup the handler and trigger initial load via {@link EnergyManagerHandler::refreshDevices}.
226      *
227      * Web request against energy manager and loading of devices is deferred and will send the ONLINE
228      * event after loading all devices.
229      */
230     @Override
231     public void initialize() {
232         SolarwattBridgeConfiguration localConfig = this.getConfigAs(SolarwattBridgeConfiguration.class);
233         this.initRefresh(localConfig);
234         this.initDeviceCache(localConfig);
235     }
236
237     private void initDeviceCache(SolarwattBridgeConfiguration localConfig) {
238         if (localConfig.hostname.isEmpty()) {
239             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Hostname is not set");
240         } else {
241             this.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
242                     "Waiting to retrieve devices.");
243             this.connector.setConfiguration(localConfig);
244
245             this.devicesCache = new ExpiringCache<>(Duration.of(localConfig.refresh, ChronoUnit.SECONDS),
246                     this::refreshDevices);
247
248             ExpiringCache<Map<String, Device>> localDevicesCache = this.devicesCache;
249             if (localDevicesCache != null) {
250                 // trigger initial load
251                 this.scheduler.execute(localDevicesCache::getValue);
252             }
253         }
254     }
255
256     /**
257      * Stop the refresh job and remove devices.
258      */
259     @Override
260     public void dispose() {
261         ScheduledFuture<?> localRefreshJob = this.refreshJob;
262         if (localRefreshJob != null && !localRefreshJob.isCancelled()) {
263             localRefreshJob.cancel(true);
264             this.refreshJob = null;
265         }
266
267         this.devicesCache = null;
268     }
269
270     private synchronized void initRefresh(SolarwattBridgeConfiguration localConfig) {
271         ScheduledFuture<?> localRefreshJob = this.refreshJob;
272         if (localRefreshJob == null || localRefreshJob.isCancelled()) {
273             this.refreshJob = this.scheduler.scheduleWithFixedDelay(this.refreshRunnable, 0, localConfig.refresh,
274                     TimeUnit.SECONDS);
275         }
276     }
277
278     /**
279      * Fetch the map of devices from the cache.
280      *
281      * Used by all childs to get their values.
282      *
283      * @return map with all {@link Device}s
284      */
285     public @Nullable Map<String, Device> getDevices() {
286         ExpiringCache<Map<String, Device>> localDevicesCache = this.devicesCache;
287         if (localDevicesCache != null) {
288             Map<String, Device> cache = localDevicesCache.getValue();
289             if (cache != null) {
290                 this.updateStatus(ThingStatus.ONLINE);
291             } else {
292                 this.updateStatus(ThingStatus.OFFLINE);
293             }
294
295             return cache;
296         } else {
297             return new HashMap<>();
298         }
299     }
300
301     /**
302      * Get a device for a specific guid.
303      *
304      * @param guid to search for
305      * @return device belonging to guid or null if not found.
306      */
307     public @Nullable Device getDeviceFromGuid(String guid) {
308         Map<String, Device> localDevices = this.getDevices();
309         if (localDevices != null && localDevices.containsKey(guid)) {
310             return localDevices.get(guid);
311         }
312
313         return null;
314     }
315
316     /**
317      * Convert the energy manager millisecond timestamps to {@link ZonedDateTime}
318      *
319      * The energy manager is the only point that knows about the timezone and
320      * it is available to all other devices. All timestamps used by all devices
321      * are in milliseconds since the epoch.
322      *
323      * @param timestamp milliseconds since the epoch
324      * @return date time in timezone
325      */
326     public ZonedDateTime getFromMilliTimestamp(BigDecimal timestamp) {
327         Map<String, Device> devices = this.getDevices();
328         if (devices != null) {
329             EnergyManager energyManager = (EnergyManager) devices.get(this.energyManagerGuid);
330             if (energyManager != null) {
331                 BigDecimal[] bigDecimals = timestamp.divideAndRemainder(BigDecimal.valueOf(1_000));
332                 Instant instant = Instant.ofEpochSecond(bigDecimals[0].longValue(),
333                         bigDecimals[1].multiply(BigDecimal.valueOf(1_000_000)).longValue());
334
335                 ZoneId localZoneID = this.zoneId;
336                 if (localZoneID != null) {
337                     return ZonedDateTime.ofInstant(instant, localZoneID);
338                 }
339             }
340         }
341
342         throw new DateTimeException("Timezone from energy manager missing.");
343     }
344
345     /**
346      * Reload all devices from the energy manager.
347      *
348      * This method is called via the {@link ExpiringCache}.
349      * 
350      * @return map from guid to {@link Device}}
351      */
352     private @Nullable Map<String, Device> refreshDevices() {
353         try {
354             final Map<String, Device> devicesData = this.connector.retrieveDevices().getDevices();
355             this.updateStatus(ThingStatus.ONLINE);
356
357             // trigger refresh of the available channels
358             if (devicesData.containsKey(this.energyManagerGuid)) {
359                 Device device = devicesData.get(this.energyManagerGuid);
360                 if (device != null) {
361                     this.initDeviceChannels(device);
362                 }
363             } else {
364                 this.logger.warn("{}: initDeviceChannels missing energy manager {}", this, this.getThing().getUID());
365             }
366
367             return devicesData;
368         } catch (final SolarwattConnectionException e) {
369             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
370         }
371
372         return null;
373     }
374
375     /**
376      * Trigger an update on all child things of this bridge.
377      */
378     private void updateAllChildThings() {
379         this.getThing().getThings().forEach(childThing -> {
380             try {
381                 ThingHandler childHandler = childThing.getHandler();
382                 if (childHandler != null) {
383                     childHandler.handleCommand(new ChannelUID(childThing.getUID(), CHANNEL_TIMESTAMP.getChannelName()),
384                             RefreshType.REFRESH);
385                 } else {
386                     this.logger.warn("no handler found for thing/device {}",
387                             childThing.getConfiguration().as(SolarwattThingConfiguration.class).guid);
388                 }
389             } catch (Exception ex) {
390                 this.logger.warn("Error processing child with uid {}", childThing.getUID(), ex);
391             }
392         });
393     }
394 }