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.weathercompany.internal.handler;
15 import static org.openhab.binding.weathercompany.internal.WeatherCompanyBindingConstants.*;
17 import java.util.Objects;
18 import java.util.concurrent.Future;
19 import java.util.concurrent.TimeUnit;
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;
42 import com.google.gson.JsonSyntaxException;
45 * The {@link WeatherCompanyObservationsHandler} is responsible for pulling Personal
46 * Weather Station (PWS) observations from the Weather Company API.
48 * API documentation is located here
49 * - https://docs.google.com/document/d/1eKCnKXI9xnoMGRRzOL1xPCBihNV2rOet08qpE_gArAY/edit
51 * @author Mark Hilbush - Initial contribution
54 public class WeatherCompanyObservationsHandler extends WeatherCompanyAbstractHandler {
55 private static final String BASE_PWS_URL = "https://api.weather.com/v2/pws/observations/current";
57 private final Logger logger = LoggerFactory.getLogger(WeatherCompanyObservationsHandler.class);
59 private int refreshIntervalSeconds;
61 private @Nullable Future<?> refreshObservationsJob;
63 private final Runnable refreshRunnable = new Runnable() {
66 refreshPwsObservations();
70 public WeatherCompanyObservationsHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient,
71 UnitProvider unitProvider, LocaleProvider localeProvider) {
72 super(thing, timeZoneProvider, httpClient, unitProvider);
76 public void initialize() {
77 logger.debug("Initializing observations handler with configuration: {}",
78 getConfigAs(WeatherCompanyObservationsConfig.class).toString());
80 refreshIntervalSeconds = getConfigAs(WeatherCompanyObservationsConfig.class).refreshInterval * 60;
81 weatherDataCache.clear();
83 updateStatus(isBridgeOnline() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
87 public void dispose() {
89 updateStatus(ThingStatus.OFFLINE);
93 public void handleCommand(ChannelUID channelUID, Command command) {
94 if (command.equals(RefreshType.REFRESH)) {
95 State state = weatherDataCache.get(channelUID.getId());
97 updateChannel(channelUID.getId(), state);
103 * Build the URL for requesting the PWS current observations
105 private @Nullable String buildPwsUrl() {
106 WeatherCompanyObservationsConfig config = getConfigAs(WeatherCompanyObservationsConfig.class);
107 String pwsStationId = config.pwsStationId;
108 if (pwsStationId == null || pwsStationId.isEmpty()) {
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));
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");
134 logger.debug("Handler: Requesting PWS observations from The Weather Company API");
135 String response = executeApiRequest(buildPwsUrl());
136 if (response == null) {
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");
154 private void updatePwsObservations(PwsObservationsDTO pwsObservations) {
155 if (pwsObservations.observations.length == 0) {
156 logger.debug("Handler: PWS observation object contains no observations!");
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));
185 * The refresh job updates the PWS current observations
186 * on the refresh interval set in the thing config
188 private void scheduleRefreshJob() {
189 logger.debug("Handler: Scheduling observations refresh job in {} seconds", REFRESH_JOB_INITIAL_DELAY_SECONDS);
191 refreshObservationsJob = scheduler.scheduleWithFixedDelay(refreshRunnable, REFRESH_JOB_INITIAL_DELAY_SECONDS,
192 refreshIntervalSeconds, TimeUnit.SECONDS);
195 private void cancelRefreshJob() {
196 if (refreshObservationsJob != null) {
197 refreshObservationsJob.cancel(true);
198 logger.debug("Handler: Canceling observations refresh job");