001/* 002 * geordi 003 * 004 * Copyright (C) 2020 Richard "Shred" Körber 005 * https://github.com/shred/geordi 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU General Public License as 009 * published by the Free Software Foundation, either version 3 of the 010 * License, or (at your option) any later version. 011 * 012 * This program is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 015 */ 016package org.shredzone.geordi.device; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.net.HttpURLConnection; 021import java.net.MalformedURLException; 022import java.net.URL; 023import java.time.Instant; 024import java.time.temporal.ChronoField; 025import java.time.temporal.ChronoUnit; 026import java.time.temporal.Temporal; 027import java.util.ArrayList; 028import java.util.List; 029import java.util.Optional; 030 031import javax.inject.Inject; 032 033import org.json.JSONArray; 034import org.json.JSONException; 035import org.json.JSONObject; 036import org.json.JSONTokener; 037import org.shredzone.geordi.GeordiException; 038import org.shredzone.geordi.data.Sample; 039import org.shredzone.geordi.sensor.Sensor; 040import org.shredzone.geordi.service.DatabaseService; 041 042/** 043 * A {@link Device} implementation for the Kaminari lightning sensor project. 044 * 045 * @see <a href="https://kaminari.shredzone.org">Kaminari project page</a> 046 */ 047public class KaminariDevice extends Device { 048 049 @Inject 050 private DatabaseService databaseService; 051 052 @Override 053 public List<Sample> readSensors() { 054 JSONObject json; 055 try (InputStream in = openConnection("status").getInputStream()) { 056 json = new JSONObject(new JSONTokener(in)); 057 } catch (IOException | JSONException ex) { 058 throw new GeordiException("Could not read data for sensor " + getId(), ex); 059 } 060 061 List<Sensor> sensors = databaseService.fetchSensors(this); 062 List<Sample> result = new ArrayList<>(); 063 064 JSONArray values = json.getJSONArray("lightnings"); 065 for (int ix = 0; ix < values.length(); ix++) { 066 JSONObject jo = values.getJSONObject(ix); 067 for (Sensor sensor : sensors) { 068 getLightningValue(jo, sensor).ifPresent(result::add); 069 } 070 } 071 072 Instant now = Instant.now(); 073 for (Sensor sensor : sensors) { 074 getSensorValue(json, sensor, now).ifPresent(result::add); 075 } 076 077 try (InputStream in = openConnection("clear").getInputStream()) { 078 while (in.read() != -1) { 079 // intentionally left empty 080 } 081 } catch (IOException ex) { 082 throw new GeordiException("Could not clear data for sensor " + getId(), ex); 083 } 084 085 return result; 086 } 087 088 private Optional<Sample> getLightningValue(JSONObject values, Sensor sensor) { 089 JSONObject config = sensor.getConfig(); 090 if (!config.has("lightning_key")) { 091 return Optional.empty(); 092 } 093 094 Instant ts = Instant.now() 095 .minus(values.getLong("age"), ChronoUnit.SECONDS) 096 .with(KaminariDevice::truncate2Seconds); 097 098 String key = config.getString("lightning_key"); 099 return Optional.ofNullable(values.optBigDecimal(key, null)) 100 .map(value -> new Sample(sensor, ts, value)); 101 } 102 103 /** 104 * Reads the value of a {@link Sensor} from the JSON response. 105 * 106 * @param values 107 * JSON response of Kaminari 108 * @param sensor 109 * {@link Sensor} to read 110 * @return Sensor value, or empty if the sensor provided no value 111 */ 112 private Optional<Sample> getSensorValue(JSONObject values, Sensor sensor, Instant now) { 113 JSONObject config = sensor.getConfig(); 114 if (!config.has("key")) { 115 return Optional.empty(); 116 } 117 118 String key = config.getString("key"); 119 return Optional.ofNullable(values.optBigDecimal(key, null)) 120 .map(value -> new Sample(sensor, now, value)); 121 } 122 123 /** 124 * Opens a connection to Kaminari. 125 */ 126 private HttpURLConnection openConnection(String target) throws IOException { 127 HttpURLConnection connection = (HttpURLConnection) getServerUrl(target).openConnection(); 128 String apikey = getConfig().getString("apikey"); 129 if (apikey != null) { 130 connection.setRequestProperty("X-API-Key", apikey); 131 } 132 return connection; 133 } 134 135 /** 136 * Returns the URL of Kaminari's web server. 137 */ 138 private URL getServerUrl(String target) { 139 try { 140 return new URL(String.format("http://%s/%s", getConfig().getString("host"), target)); 141 } catch (MalformedURLException | JSONException ex) { 142 throw new GeordiException("Bad host config", ex); 143 } 144 } 145 146 /** 147 * Truncates the {@link Temporal} at "2 seconds" intervals. 148 * 149 * @param temporal 150 * {@link Temporal} to truncate 151 * @return Truncated {@link Temporal} 152 */ 153 private static Temporal truncate2Seconds(Temporal temporal) { 154 long time = temporal.getLong(ChronoField.INSTANT_SECONDS); 155 time = ((time + 1L) / 2L) * 2L; 156 return Instant.ofEpochSecond(time); 157 } 158 159}