2 * Copyright (c) 2010-2024 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.airgradient.internal.handler;
15 import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CHANNEL_CALIBRATION;
16 import static org.openhab.binding.airgradient.internal.AirGradientBindingConstants.CHANNEL_LEDS_MODE;
18 import java.util.List;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.client.HttpClient;
26 import org.openhab.binding.airgradient.internal.communication.AirGradientCommunicationException;
27 import org.openhab.binding.airgradient.internal.communication.RemoteAPIController;
28 import org.openhab.binding.airgradient.internal.config.AirGradientAPIConfiguration;
29 import org.openhab.binding.airgradient.internal.model.Measure;
30 import org.openhab.core.library.types.StringType;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.binding.BaseThingHandler;
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.Gson;
45 * The {@link AirGradientAPIHandler} is responsible for handling commands, which are
46 * sent to one of the channels.
48 * @author Jørgen Austvik - Initial contribution
51 public class AirGradientLocalHandler extends BaseThingHandler {
53 private final Logger logger = LoggerFactory.getLogger(AirGradientLocalHandler.class);
55 private @Nullable ScheduledFuture<?> pollingJob;
56 private final HttpClient httpClient;
57 private final Gson gson;
59 private @NonNullByDefault({}) RemoteAPIController apiController = null;
60 private @NonNullByDefault({}) AirGradientAPIConfiguration apiConfig = null;
62 public AirGradientLocalHandler(Thing thing, HttpClient httpClient) {
64 this.httpClient = httpClient;
65 this.gson = new Gson();
69 public void handleCommand(ChannelUID channelUID, Command command) {
70 logger.debug("Channel {}: {}", channelUID, command.toFullString());
71 if (command instanceof RefreshType) {
73 } else if (CHANNEL_LEDS_MODE.equals(channelUID.getId())) {
74 if (command instanceof StringType stringCommand) {
75 setLedModeOnDevice(stringCommand.toFullString());
77 logger.warn("Received command {} for channel {}, but it needs a string command", command.toString(),
80 } else if (CHANNEL_CALIBRATION.equals(channelUID.getId())) {
81 if (command instanceof StringType stringCommand) {
82 if ("co2".equals(stringCommand.toFullString())) {
83 calibrateCo2OnDevice();
86 "Received unknown command {} for calibration on channel {}, which we don't know how to handle",
87 command.toString(), channelUID.getId());
92 logger.warn("Received command {} for channel {}, which we don't know how to handle", command.toString(),
98 public void initialize() {
99 apiConfig = getConfigAs(AirGradientAPIConfiguration.class);
100 if (!apiConfig.isValid()) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
102 "Need to set hostname to a valid URL. Refresh interval needs to be a positive integer.");
106 apiController = new RemoteAPIController(httpClient, gson, apiConfig);
108 // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
109 // the framework is then able to reuse the resources from the thing handler initialization.
110 // we set this upfront to reliably check status updates in unit tests.
111 updateStatus(ThingStatus.UNKNOWN);
113 pollingJob = scheduler.scheduleWithFixedDelay(this::pollingCode, 0, apiConfig.refreshInterval,
117 protected void pollingCode() {
119 List<Measure> measures = apiController.getMeasures();
120 updateStatus(ThingStatus.ONLINE);
122 if (measures.size() != 1) {
123 logger.warn("Expecting single set of measures for local device, but got {} measures", measures.size());
127 updateProperties(MeasureHelper.createProperties(measures.get(0)));
128 Map<String, State> states = MeasureHelper.createStates(measures.get(0));
129 for (Map.Entry<String, State> entry : states.entrySet()) {
130 if (isLinked(entry.getKey())) {
131 updateState(entry.getKey(), entry.getValue());
134 } catch (AirGradientCommunicationException agce) {
135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
139 private void setLedModeOnDevice(String mode) {
141 apiController.setLedMode(getSerialNo(), mode);
142 updateStatus(ThingStatus.ONLINE);
143 } catch (AirGradientCommunicationException agce) {
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
148 private void calibrateCo2OnDevice() {
150 apiController.calibrateCo2(getSerialNo());
151 updateStatus(ThingStatus.ONLINE);
152 } catch (AirGradientCommunicationException agce) {
153 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, agce.getMessage());
158 * Returns the serial number of this sensor.
160 * @return serial number of this sensor.
162 public String getSerialNo() {
163 String serialNo = thing.getProperties().get(Thing.PROPERTY_SERIAL_NUMBER);
164 if (serialNo == null) {
172 public void dispose() {
173 ScheduledFuture<?> pollingJob = this.pollingJob;
174 if (pollingJob != null) {
175 pollingJob.cancel(true);
176 this.pollingJob = null;
180 protected void setConfiguration(AirGradientAPIConfiguration config) {
181 this.apiConfig = config;