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.solarwatt.internal.handler;
15 import static org.openhab.binding.solarwatt.internal.SolarwattBindingConstants.*;
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.Collections;
26 import java.util.HashMap;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
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;
60 * The {@link EnergyManagerHandler} is responsible for handling energy manager thing itself
61 * and handle data retrieval for the child things.
63 * @author Sven Carstens - Initial contribution
66 public class EnergyManagerHandler extends BaseBridgeHandler {
68 private final Logger logger = LoggerFactory.getLogger(EnergyManagerHandler.class);
70 private final EnergyManagerConnector connector;
71 private final SolarwattChannelTypeProvider channelTypeProvider;
73 private @Nullable ExpiringCache<Map<String, Device>> devicesCache;
74 private @Nullable ScheduledFuture<?> refreshJob;
76 private @Nullable ZoneId zoneId;
79 * Guid of this energy manager itself.
81 private @Nullable String energyManagerGuid;
84 * Runner for the {@link ExpiringCache} refresh.
86 * Triggers update of all child things.
88 private final Runnable refreshRunnable = () -> {
89 EnergyManagerHandler.this.updateChannels();
90 EnergyManagerHandler.this.updateAllChildThings();
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
100 public EnergyManagerHandler(final Bridge thing, final SolarwattChannelTypeProvider channelTypeProvider,
101 final HttpClient httpClient) {
103 this.connector = new EnergyManagerConnector(httpClient);
104 this.channelTypeProvider = channelTypeProvider;
108 * Get services which are provided by this handler.
110 * Only service discovery is provided by
112 * @return collection containing our discovery service
115 public Collection<Class<? extends ThingHandlerService>> getServices() {
116 return Collections.singleton(SolarwattDevicesDiscoveryService.class);
120 * Execute the desired commands.
122 * Only refresh is supported and relayed to all childs of this thing.
124 * @param channelUID for which the command is issued
125 * @param command command issued
128 public void handleCommand(ChannelUID channelUID, Command command) {
129 if (command instanceof RefreshType) {
130 this.updateChannels();
135 * Dynnamically updates all known channel states of the energy manager.
137 public void updateChannels() {
138 Map<String, Device> devices = this.getDevices();
139 if (devices != null) {
140 if (this.energyManagerGuid == null) {
142 this.findEnergyManagerGuid(devices);
143 } catch (SolarwattConnectionException ex) {
144 this.logger.warn("Failed updating EnergyManager channels: {}", ex.getMessage());
147 EnergyManager energyManager = (EnergyManager) devices.get(this.energyManagerGuid);
149 if (energyManager != null) {
150 this.calculateUpdates(energyManager);
152 energyManager.getStateValues().forEach((stateName, stateValue) -> {
153 this.updateState(stateName, stateValue);
156 this.logger.warn("updateChannels failed, missing device EnergyManager {}", this.energyManagerGuid);
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());
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)));
175 * Initial setup of the channels available for this thing.
177 * @param device which provides the channels
179 protected void initDeviceChannels(Device device) {
180 this.assertChannel(new SolarwattChannel(CHANNEL_DATETIME.getChannelName(), "time"));
182 device.getSolarwattChannelSet().forEach((channelTag, solarwattChannel) -> {
183 this.assertChannel(solarwattChannel);
188 * Assert that all channels inside of our thing are well defined.
190 * Only channel which can not be found are created.
192 * @param solarwattChannel channel description with name and unit
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());
202 this.updateThing(thingBuilder.build());
207 * Finds the guid of the energy manager inside of the known devices.
209 * @param devices list with known devices
210 * @throws SolarwattConnectionException if there is no energy manager available
212 private void findEnergyManagerGuid(Map<String, Device> devices) throws SolarwattConnectionException {
213 devices.forEach((guid, device) -> {
214 if (device instanceof EnergyManager) {
215 this.energyManagerGuid = guid;
219 if (this.energyManagerGuid == null) {
220 throw new SolarwattConnectionException("unable to find energy manager");
225 * Setup the handler and trigger initial load via {@link EnergyManagerHandler::refreshDevices}.
227 * Web request against energy manager and loading of devices is deferred and will send the ONLINE
228 * event after loading all devices.
231 public void initialize() {
232 SolarwattBridgeConfiguration localConfig = this.getConfigAs(SolarwattBridgeConfiguration.class);
233 this.initRefresh(localConfig);
234 this.initDeviceCache(localConfig);
237 private void initDeviceCache(SolarwattBridgeConfiguration localConfig) {
238 if (localConfig.hostname.isEmpty()) {
239 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Hostname is not set");
241 this.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
242 "Waiting to retrieve devices.");
243 this.connector.setConfiguration(localConfig);
245 this.devicesCache = new ExpiringCache<>(Duration.of(localConfig.refresh, ChronoUnit.SECONDS),
246 this::refreshDevices);
248 ExpiringCache<Map<String, Device>> localDevicesCache = this.devicesCache;
249 if (localDevicesCache != null) {
250 // trigger initial load
251 this.scheduler.execute(localDevicesCache::getValue);
257 * Stop the refresh job and remove devices.
260 public void dispose() {
261 ScheduledFuture<?> localRefreshJob = this.refreshJob;
262 if (localRefreshJob != null && !localRefreshJob.isCancelled()) {
263 localRefreshJob.cancel(true);
264 this.refreshJob = null;
267 this.devicesCache = null;
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,
279 * Fetch the map of devices from the cache.
281 * Used by all childs to get their values.
283 * @return map with all {@link Device}s
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();
290 this.updateStatus(ThingStatus.ONLINE);
292 this.updateStatus(ThingStatus.OFFLINE);
297 return new HashMap<>();
302 * Get a device for a specific guid.
304 * @param guid to search for
305 * @return device belonging to guid or null if not found.
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);
317 * Convert the energy manager millisecond timestamps to {@link ZonedDateTime}
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.
323 * @param timestamp milliseconds since the epoch
324 * @return date time in timezone
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());
335 ZoneId localZoneID = this.zoneId;
336 if (localZoneID != null) {
337 return ZonedDateTime.ofInstant(instant, localZoneID);
342 throw new DateTimeException("Timezone from energy manager missing.");
346 * Reload all devices from the energy manager.
348 * This method is called via the {@link ExpiringCache}.
350 * @return map from guid to {@link Device}}
352 private @Nullable Map<String, Device> refreshDevices() {
354 final Map<String, Device> devicesData = this.connector.retrieveDevices().getDevices();
355 this.updateStatus(ThingStatus.ONLINE);
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);
364 this.logger.warn("{}: initDeviceChannels missing energy manager {}", this, this.getThing().getUID());
368 } catch (final SolarwattConnectionException e) {
369 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
376 * Trigger an update on all child things of this bridge.
378 private void updateAllChildThings() {
379 this.getThing().getThings().forEach(childThing -> {
381 ThingHandler childHandler = childThing.getHandler();
382 if (childHandler != null) {
383 childHandler.handleCommand(new ChannelUID(childThing.getUID(), CHANNEL_TIMESTAMP.getChannelName()),
384 RefreshType.REFRESH);
386 this.logger.warn("no handler found for thing/device {}",
387 childThing.getConfiguration().as(SolarwattThingConfiguration.class).guid);
389 } catch (Exception ex) {
390 this.logger.warn("Error processing child with uid {}", childThing.getUID(), ex);