]> git.basschouten.com Git - openhab-addons.git/blob
287f32f2287ebdefde67026b13c6e1c2e6db4ceb
[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.weathercompany.internal.handler;
14
15 import static org.openhab.binding.weathercompany.internal.WeatherCompanyBindingConstants.*;
16
17 import java.util.Objects;
18 import java.util.concurrent.Future;
19 import java.util.concurrent.TimeUnit;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.client.HttpClient;
24 import org.openhab.binding.weathercompany.internal.config.WeatherCompanyObservationsConfig;
25 import org.openhab.binding.weathercompany.internal.model.PwsObservationsDTO;
26 import org.openhab.binding.weathercompany.internal.model.PwsObservationsDTO.Observations;
27 import org.openhab.core.i18n.LocaleProvider;
28 import org.openhab.core.i18n.TimeZoneProvider;
29 import org.openhab.core.i18n.UnitProvider;
30 import org.openhab.core.library.unit.ImperialUnits;
31 import org.openhab.core.library.unit.Units;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.types.Command;
37 import org.openhab.core.types.RefreshType;
38 import org.openhab.core.types.State;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 import com.google.gson.JsonSyntaxException;
43
44 /**
45  * The {@link WeatherCompanyObservationsHandler} is responsible for pulling Personal
46  * Weather Station (PWS) observations from the Weather Company API.
47  *
48  * API documentation is located here
49  * - https://docs.google.com/document/d/1eKCnKXI9xnoMGRRzOL1xPCBihNV2rOet08qpE_gArAY/edit
50  *
51  * @author Mark Hilbush - Initial contribution
52  */
53 @NonNullByDefault
54 public class WeatherCompanyObservationsHandler extends WeatherCompanyAbstractHandler {
55     private static final String BASE_PWS_URL = "https://api.weather.com/v2/pws/observations/current";
56
57     private final Logger logger = LoggerFactory.getLogger(WeatherCompanyObservationsHandler.class);
58
59     private int refreshIntervalSeconds;
60
61     private @Nullable Future<?> refreshObservationsJob;
62
63     private final Runnable refreshRunnable = new Runnable() {
64         @Override
65         public void run() {
66             refreshPwsObservations();
67         }
68     };
69
70     public WeatherCompanyObservationsHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient,
71             UnitProvider unitProvider, LocaleProvider localeProvider) {
72         super(thing, timeZoneProvider, httpClient, unitProvider);
73     }
74
75     @Override
76     public void initialize() {
77         logger.debug("Initializing observations handler with configuration: {}",
78                 getConfigAs(WeatherCompanyObservationsConfig.class).toString());
79
80         refreshIntervalSeconds = getConfigAs(WeatherCompanyObservationsConfig.class).refreshInterval * 60;
81         weatherDataCache.clear();
82         scheduleRefreshJob();
83         updateStatus(isBridgeOnline() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
84     }
85
86     @Override
87     public void dispose() {
88         cancelRefreshJob();
89         updateStatus(ThingStatus.OFFLINE);
90     }
91
92     @Override
93     public void handleCommand(ChannelUID channelUID, Command command) {
94         if (command.equals(RefreshType.REFRESH)) {
95             State state = weatherDataCache.get(channelUID.getId());
96             if (state != null) {
97                 updateChannel(channelUID.getId(), state);
98             }
99         }
100     }
101
102     /*
103      * Build the URL for requesting the PWS current observations
104      */
105     private @Nullable String buildPwsUrl() {
106         WeatherCompanyObservationsConfig config = getConfigAs(WeatherCompanyObservationsConfig.class);
107         String pwsStationId = config.pwsStationId;
108         if (pwsStationId == null || pwsStationId.isEmpty()) {
109             return null;
110         }
111         String apiKey = getApiKey();
112         StringBuilder sb = new StringBuilder(BASE_PWS_URL);
113         // Set to use Imperial units. UoM will convert to the other units
114         sb.append("?units=e");
115         // Get temperatures with one decimal point precision
116         sb.append("&numericPrecision=decimal");
117         // Set response type as JSON
118         sb.append("&format=json");
119         // Set PWS station Id from config
120         sb.append("&stationId=").append(pwsStationId);
121         // Set API key from config
122         sb.append("&apiKey=").append(apiKey);
123         String url = sb.toString();
124         logger.debug("PWS observations URL is {}", url.replace(apiKey, REPLACE_API_KEY));
125         return url;
126     }
127
128     private synchronized void refreshPwsObservations() {
129         if (!isBridgeOnline()) {
130             // If bridge is not online, API has not been validated yet
131             logger.debug("Handler: Can't refresh PWS observations because bridge is not online");
132             return;
133         }
134         logger.debug("Handler: Requesting PWS observations from The Weather Company API");
135         String response = executeApiRequest(buildPwsUrl());
136         if (response == null) {
137             return;
138         }
139         try {
140             logger.debug("Handler: Parsing PWS observations response: {}", response);
141             PwsObservationsDTO pwsObservations = Objects
142                     .requireNonNull(gson.fromJson(response, PwsObservationsDTO.class));
143             logger.debug("Handler: Successfully parsed PWS observations response object");
144             updateStatus(ThingStatus.ONLINE);
145             updatePwsObservations(pwsObservations);
146         } catch (JsonSyntaxException e) {
147             logger.debug("Handler: Error parsing pws observations response object: {}", e.getMessage(), e);
148             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
149                     "@text/offline.comm-error-parsing-pws-forecast");
150             return;
151         }
152     }
153
154     private void updatePwsObservations(PwsObservationsDTO pwsObservations) {
155         if (pwsObservations.observations.length == 0) {
156             logger.debug("Handler: PWS observation object contains no observations!");
157             return;
158         }
159         Observations obs = pwsObservations.observations[0];
160         logger.debug("Handler: Processing observations from station {} at {}", obs.stationID, obs.obsTimeLocal);
161         updateChannel(CH_PWS_TEMP, undefOrQuantity(obs.imperial.temp, ImperialUnits.FAHRENHEIT));
162         updateChannel(CH_PWS_TEMP_HEAT_INDEX, undefOrQuantity(obs.imperial.heatIndex, ImperialUnits.FAHRENHEIT));
163         updateChannel(CH_PWS_TEMP_WIND_CHILL, undefOrQuantity(obs.imperial.windChill, ImperialUnits.FAHRENHEIT));
164         updateChannel(CH_PWS_TEMP_DEW_POINT, undefOrQuantity(obs.imperial.dewpt, ImperialUnits.FAHRENHEIT));
165         updateChannel(CH_PWS_HUMIDITY, undefOrQuantity(obs.humidity, Units.PERCENT));
166         updateChannel(CH_PWS_PRESSURE, undefOrQuantity(obs.imperial.pressure, ImperialUnits.INCH_OF_MERCURY));
167         updateChannel(CH_PWS_PRECIPTATION_RATE, undefOrQuantity(obs.imperial.precipRate, Units.INCHES_PER_HOUR));
168         updateChannel(CH_PWS_PRECIPITATION_TOTAL, undefOrQuantity(obs.imperial.precipTotal, ImperialUnits.INCH));
169         updateChannel(CH_PWS_WIND_SPEED, undefOrQuantity(obs.imperial.windSpeed, ImperialUnits.MILES_PER_HOUR));
170         updateChannel(CH_PWS_WIND_GUST, undefOrQuantity(obs.imperial.windGust, ImperialUnits.MILES_PER_HOUR));
171         updateChannel(CH_PWS_WIND_DIRECTION, undefOrQuantity(obs.winddir, Units.DEGREE_ANGLE));
172         updateChannel(CH_PWS_SOLAR_RADIATION, undefOrQuantity(obs.solarRadiation, Units.IRRADIANCE));
173         updateChannel(CH_PWS_UV, undefOrDecimal(obs.uv));
174         updateChannel(CH_PWS_OBSERVATION_TIME_LOCAL, undefOrDate(obs.obsTimeUtc));
175         updateChannel(CH_PWS_NEIGHBORHOOD, undefOrString(obs.neighborhood));
176         updateChannel(CH_PWS_STATION_ID, undefOrString(obs.stationID));
177         updateChannel(CH_PWS_COUNTRY, undefOrString(obs.country));
178         updateChannel(CH_PWS_LOCATION, undefOrPoint(obs.lat, obs.lon));
179         updateChannel(CH_PWS_ELEVATION, undefOrQuantity(obs.imperial.elev, ImperialUnits.FOOT));
180         updateChannel(CH_PWS_QC_STATUS, undefOrDecimal(obs.qcStatus));
181         updateChannel(CH_PWS_SOFTWARE_TYPE, undefOrString(obs.softwareType));
182     }
183
184     /*
185      * The refresh job updates the PWS current observations
186      * on the refresh interval set in the thing config
187      */
188     private void scheduleRefreshJob() {
189         logger.debug("Handler: Scheduling observations refresh job in {} seconds", REFRESH_JOB_INITIAL_DELAY_SECONDS);
190         cancelRefreshJob();
191         refreshObservationsJob = scheduler.scheduleWithFixedDelay(refreshRunnable, REFRESH_JOB_INITIAL_DELAY_SECONDS,
192                 refreshIntervalSeconds, TimeUnit.SECONDS);
193     }
194
195     private void cancelRefreshJob() {
196         if (refreshObservationsJob != null) {
197             refreshObservationsJob.cancel(true);
198             logger.debug("Handler: Canceling observations refresh job");
199         }
200     }
201 }