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.eclipse.jdt.annotation.Checks.requireNonNull;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.hamcrest.Matchers.closeTo;
18 import static org.hamcrest.Matchers.empty;
19 import static org.hamcrest.Matchers.is;
20 import static org.hamcrest.Matchers.not;
21 import static org.mockito.ArgumentMatchers.anyString;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.client.HttpClient;
26 import org.eclipse.jetty.client.api.ContentResponse;
27 import org.eclipse.jetty.client.api.Request;
28 import org.junit.jupiter.api.Assertions;
29 import org.junit.jupiter.api.BeforeEach;
30 import org.junit.jupiter.api.Test;
31 import org.mockito.Mockito;
32 import org.openhab.binding.airgradient.internal.communication.AirGradientCommunicationException;
33 import org.openhab.binding.airgradient.internal.communication.RemoteAPIController;
34 import org.openhab.binding.airgradient.internal.config.AirGradientAPIConfiguration;
36 import com.google.gson.Gson;
39 * @author Jørgen Austvik - Initial contribution
41 @SuppressWarnings({ "null" })
43 public class RemoteApiControllerTest {
45 private static final AirGradientAPIConfiguration TEST_CONFIG = new AirGradientAPIConfiguration() {
52 private static final String SINGLE_CONTENT = """
53 {"locationId":4321,"locationName":"Some other name","pm01":1,"pm02":2,"pm10":2,"pm003Count":536,"atmp":20.45,"rhum":16.61,"rco2":456,"tvoc":51.644928,"wifi":-54,"timestamp":"2024-01-07T13:00:20.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"serial","firmwareVersion":null,"tvocIndex":null,"noxIndex":null}
56 private static final String MULTI_CONTENT = """
58 {"locationId":1234,"locationName":"Some Name","pm01":1,"pm02":2,"pm10":2,"pm003Count":536,"atmp":20.45,"rhum":16.61,"rco2":null,"tvoc":null,"wifi":-54,"timestamp":"2024-01-07T13:00:20.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"serial","firmwareVersion":null,"tvocIndex":null,"noxIndex":null},
59 {"locationId":4321,"locationName":"Some other name","pm01":1,"pm02":2,"pm10":2,"pm003Count":536,"atmp":20.45,"rhum":16.61,"rco2":null,"tvoc":null,"wifi":-54,"timestamp":"2024-01-07T13:00:20.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"serial","firmwareVersion":null,"tvocIndex":null,"noxIndex":null}
61 private static final String MULTI_CONTENT2 = """
62 [{"locationId":654321,"locationName":"xxxx","pm01":0,"pm02":1,"pm10":1,"pm003Count":null,"atmp":24.2,"rhum":18,"rco2":519,"tvoc":50.793266,"wifi":-62,"timestamp":"2024-02-01T19:15:37.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"ecda3b1a2a50","firmwareVersion":null,"tvocIndex":52,"noxIndex":1},{"locationId":123456,"locationName":"yyyy","pm01":0,"pm02":0,"pm10":0,"pm003Count":105,"atmp":22.33,"rhum":24,"rco2":468,"tvoc":130.95694,"wifi":-50,"timestamp":"2024-02-01T19:15:34.000Z","ledMode":"co2","ledCo2Threshold1":1000,"ledCo2Threshold2":2000,"ledCo2ThresholdEnd":4000,"serialno":"84fce612e644","firmwareVersion":null,"tvocIndex":137,"noxIndex":1}]
65 private static final String PROMETHEUS_CONTENT = """
66 # HELP pm02 Particulate Matter PM2.5 value
68 pm02{id="Airgradient"}6
69 # HELP rco2 CO2 value, in ppm
71 rco2{id="Airgradient"}862
72 # HELP atmp Temperature, in degrees Celsius
74 atmp{id="Airgradient"}31.6
75 # HELP rhum Relative humidity, in percent
77 rhum{id="Airgradient"}38
78 # HELP tvoc Total volatile organic components, in μg/m³
80 tvoc{id="Airgradient"}51.644928
83 nox{id="Airgradient"}1
86 private static final String OPEN_METRICS_CONTENT = """
87 # HELP airgradient_info AirGradient device information
88 # TYPE airgradient_info info
89 airgradient_info{airgradient_serial_number="4XXXXXXXXXXc",airgradient_device_type="ONE_I-9PSL",airgradient_library_version="3.0.4"} 1
90 # HELP airgradient_config_ok 1 if the AirGradient device was able to successfully fetch its configuration from the server
91 # TYPE airgradient_config_ok gauge
92 airgradient_config_ok{} 1
93 # HELP airgradient_post_ok 1 if the AirGradient device was able to successfully send to the server
94 # TYPE airgradient_post_ok gauge
95 airgradient_post_ok{} 1
96 # HELP airgradient_wifi_rssi_dbm WiFi signal strength from the AirGradient device perspective, in dBm
97 # TYPE airgradient_wifi_rssi_dbm gauge
98 # UNIT airgradient_wifi_rssi_dbm dbm
99 airgradient_wifi_rssi_dbm{} -51
100 # HELP airgradient_co2_ppm Carbon dioxide concentration as measured by the AirGradient S8 sensor, in parts per million
101 # TYPE airgradient_co2_ppm gauge
102 # UNIT airgradient_co2_ppm ppm
103 airgradient_co2_ppm{} 589
104 # HELP airgradient_pm1_ugm3 PM1.0 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter
105 # TYPE airgradient_pm1_ugm3 gauge
106 # UNIT airgradient_pm1_ugm3 ugm3
107 airgradient_pm1_ugm3{} 3
108 # HELP airgradient_pm2d5_ugm3 PM2.5 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter
109 # TYPE airgradient_pm2d5_ugm3 gauge
110 # UNIT airgradient_pm2d5_ugm3 ugm3
111 airgradient_pm2d5_ugm3{} 3
112 # HELP airgradient_pm10_ugm3 PM10 concentration as measured by the AirGradient PMS sensor, in micrograms per cubic meter
113 # TYPE airgradient_pm10_ugm3 gauge
114 # UNIT airgradient_pm10_ugm3 ugm3
115 airgradient_pm10_ugm3{} 3
116 # HELP airgradient_pm0d3_p100ml PM0.3 concentration as measured by the AirGradient PMS sensor, in number of particules per 100 milliliters
117 # TYPE airgradient_pm0d3_p100ml gauge
118 # UNIT airgradient_pm0d3_p100ml p100ml
119 airgradient_pm0d3_p100ml{} 594
120 # HELP airgradient_tvoc_index The processed Total Volatile Organic Compounds (TVOC) index as measured by the AirGradient SGP sensor
121 # TYPE airgradient_tvoc_index gauge
122 airgradient_tvoc_index{} 220
123 # HELP airgradient_tvoc_raw_index The raw input value to the Total Volatile Organic Compounds (TVOC) index as measured by the AirGradient SGP sensor
124 # TYPE airgradient_tvoc_raw_index gauge
125 airgradient_tvoc_raw_index{} 30801
126 # HELP airgradient_nox_index The processed Nitrous Oxide (NOx) index as measured by the AirGradient SGP sensor
127 # TYPE airgradient_nox_index gauge
128 airgradient_nox_index{} 1
129 # HELP airgradient_temperature_degc The ambient temperature as measured by the AirGradient SHT sensor, in degrees Celsius
130 # TYPE airgradient_temperature_degc gauge
131 # UNIT airgradient_temperature_degc degc
132 airgradient_temperature_degc{} 23.69
133 # HELP airgradient_humidity_percent The relative humidity as measured by the AirGradient SHT sensor
134 # TYPE airgradient_humidity_percent gauge
135 # UNIT airgradient_humidity_percent percent
136 airgradient_humidity_percent{} 39
141 private RemoteAPIController sut;
144 HttpClient httpClientMock;
150 public void setUp() {
151 httpClientMock = Mockito.mock(HttpClient.class);
152 requestMock = Mockito.mock(Request.class);
154 sut = new RemoteAPIController(requireNonNull(httpClientMock), new Gson(), TEST_CONFIG);
158 public void testGetMeasuresNone() throws Exception {
159 ContentResponse response = Mockito.mock(ContentResponse.class);
160 Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
161 Mockito.when(requestMock.send()).thenReturn(response);
162 Mockito.when(response.getMediaType()).thenReturn("application/json");
163 Mockito.when(response.getStatus()).thenReturn(500);
165 AirGradientCommunicationException agce = Assertions.assertThrows(AirGradientCommunicationException.class,
166 () -> sut.getMeasures());
167 assertThat(agce.getMessage(), is("Returned status code: 500"));
171 public void testGetMeasuresSingle() throws Exception {
172 ContentResponse response = Mockito.mock(ContentResponse.class);
173 Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
174 Mockito.when(requestMock.send()).thenReturn(response);
175 Mockito.when(response.getStatus()).thenReturn(200);
176 Mockito.when(response.getMediaType()).thenReturn("application/json");
177 Mockito.when(response.getContentAsString()).thenReturn(SINGLE_CONTENT);
179 var res = sut.getMeasures();
180 assertThat(res, is(not(empty())));
181 assertThat(res.size(), is(1));
182 assertThat(res.get(0).locationName, is("Some other name"));
186 public void testGetMeasuresMulti() throws Exception {
187 ContentResponse response = Mockito.mock(ContentResponse.class);
188 Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
189 Mockito.when(requestMock.send()).thenReturn(response);
190 Mockito.when(response.getStatus()).thenReturn(200);
191 Mockito.when(response.getMediaType()).thenReturn("application/json");
192 Mockito.when(response.getContentAsString()).thenReturn(MULTI_CONTENT);
194 var res = sut.getMeasures();
195 assertThat(res, is(not(empty())));
196 assertThat(res.size(), is(2));
197 assertThat(res.get(0).locationName, is("Some Name"));
198 assertThat(res.get(1).locationName, is("Some other name"));
202 public void testGetMeasuresMulti2() throws Exception {
203 ContentResponse response = Mockito.mock(ContentResponse.class);
204 Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
205 Mockito.when(requestMock.send()).thenReturn(response);
206 Mockito.when(response.getStatus()).thenReturn(200);
207 Mockito.when(response.getMediaType()).thenReturn("application/json");
208 Mockito.when(response.getContentAsString()).thenReturn(MULTI_CONTENT2);
210 var res = sut.getMeasures();
211 assertThat(res, is(not(empty())));
212 assertThat(res.size(), is(2));
213 assertThat(res.get(0).locationName, is("xxxx"));
214 assertThat(res.get(1).locationName, is("yyyy"));
218 public void testGetMeasuresPrometheus() throws Exception {
219 ContentResponse response = Mockito.mock(ContentResponse.class);
220 Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
221 Mockito.when(requestMock.send()).thenReturn(response);
222 Mockito.when(response.getStatus()).thenReturn(200);
223 Mockito.when(response.getMediaType()).thenReturn("text/plain");
224 Mockito.when(response.getContentAsString()).thenReturn(PROMETHEUS_CONTENT);
226 var res = sut.getMeasures();
227 assertThat(res, is(not(empty())));
228 assertThat(res.size(), is(1));
229 assertThat(res.get(0).pm02, closeTo(6, 0.1));
230 assertThat(res.get(0).rco2, closeTo(862, 0.1));
231 assertThat(res.get(0).atmp, closeTo(31.6, 0.1));
232 assertThat(res.get(0).rhum, closeTo(38, 0.1));
233 assertThat(res.get(0).tvoc, closeTo(51.644928, 0.1));
234 assertThat(res.get(0).noxIndex, closeTo(1, 0.1));
235 assertThat(res.get(0).locationId, is("Airgradient"));
236 assertThat(res.get(0).locationName, is("Airgradient"));
237 assertThat(res.get(0).serialno, is("Airgradient"));
241 public void testGetMeasuresOpenMetrics() throws Exception {
242 ContentResponse response = Mockito.mock(ContentResponse.class);
243 Mockito.when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
244 Mockito.when(requestMock.send()).thenReturn(response);
245 Mockito.when(response.getStatus()).thenReturn(200);
246 Mockito.when(response.getMediaType()).thenReturn("application/openmetrics-text");
247 Mockito.when(response.getContentAsString()).thenReturn(OPEN_METRICS_CONTENT);
249 var res = sut.getMeasures();
250 assertThat(res, is(not(empty())));
251 assertThat(res.size(), is(1));
252 assertThat(res.get(0).pm01, closeTo(3, 0.1));
253 assertThat(res.get(0).pm02, closeTo(3, 0.1));
254 assertThat(res.get(0).pm10, closeTo(3, 0.1));
255 assertThat(res.get(0).rco2, closeTo(589, 0.1));
256 assertThat(res.get(0).atmp, closeTo(23.69, 0.1));
257 assertThat(res.get(0).rhum, closeTo(39, 0.1));
258 assertThat(res.get(0).tvoc, closeTo(220, 0.1));
259 assertThat(res.get(0).noxIndex, closeTo(1, 0.1));
260 assertThat(res.get(0).serialno, is("4XXXXXXXXXXc"));