
База данных никогда не помешает, попробуем хранить данные об измерениях температуры. Посмотрим получится ли использовать influxdb на Raspberry Pi.
На момент написания пакет dev-lang/go имел версию 1.10.1, которая не работала на ARM, так что пришлось вносить правки в конфигурационные файлы1 и устанавливать go следующим образом:
emerge -1 '=dev-lang/go-9999'
emerge dev-db/influxdb
Проверяем:

Есть соблазн быстренько набросать простейшую базу, заняться интеграцией её с Kotlin, а потом с какой-либо системой для построения красивых графиков… Стоп. Мы имеем дело с базой данных, пусть для учебных целей, но сразу тренируемся защищаться от несанкционированного доступа.
Создаём суперпользователя (имя пользователя и пароль изменены):
> create user spade with password 'super-password' with all privileges;
В файле /etc/influxdb/influxd.conf правим строчку auth-enabled = true и перезапускаем демон. После этого проверяем работоспособность авторизации:

Для моих целей достаточно самоподписанного сертификата (не забываем указать CN как localhost):
ROOT@pi64 ~# openssl req -x509 -nodes -newkey rsa:2048 \
-keyout /etc/ssl/influxdb-selfsigned.key \
-out /etc/ssl/influxdb-selfsigned.crt -days 360
Добавляем этот сертификат в хранилище Java (пароль changeit или установленный вами):
cd /etc/ssl
openssl x509 -in influxdb-selfsigned.crt -outform der -out influxdb-selfsigned.der
cd /usr/lib64/icedtea8/bin
./keytool -import -alias mykeyroot -keystore /usr/lib64/icedtea8/jre/lib/security/cacerts -file /etc/ssl/influxdb-selfsigned.der
Правим /etc/influxdb/influxdb.conf:
https-enabled = true
https-certificate = "/etc/ssl/influxdb-selfsigned.crt"
https-private-key = "/etc/ssl/influxdb-selfsigned.key"
После перезапуска демона проверяем из-под обычного пользователя:

Нужно добавить influx в зависимости в build.gradle:
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5'
compile 'org.influxdb:influxdb-java:2.9'
}
Для начала крохотная программка для проверки соединения с базой данных:
import org.influxdb.InfluxDBFactory
import kotlin.system.exitProcess
fun main(args: Array<String>) {
println("*** Raspberry Pi Influxdb ***")
val influxDB = InfluxDBFactory.connect(DB_SERVER, DB_USER, DB_PASS)
influxDB.run {
println("Connected to db.")
close()
}
exitProcess(0)
}
const val DB_SERVER = "https://localhost:8086"
const val DB_USER = "spade"
const val DB_PASS = "*******"
Результат:

Возвращаемся к консоли и создаём нашу базу данных и специального пользователя, который может только писать данные в базу:
rabbit@pi64 ~/src/yrabbit-java/thermdb % influx -ssl -unsafeSsl -host localhost
Connected to https://localhost:8086 version unknown
InfluxDB shell version: unknown
> auth
username: spade
password:
> create database thermdb
> create user sensor with password '********'
> grant write on thermdb to sensor
> create user grafana with password '********'
> grant read on thermdb to grafana
> show users
user admin
---- -----
spade true
sensor false
grafana false
> exit
Измерение temps имеет очень простую структуру:
| Поле/тэг | Тип |
|---|---|
| sensor_id | string |
| temp | float |
Устанавливаем такую политику по умолчанию, что сырые данные хранятся 2 часа, упаковываются в 15-ти минутные интервалы, хранятся месяц, упаковываются в часовые интервалы и через 4 года удаляются совсем:
> create retention policy two_hours on thermdb duration 2h replication 1 default
> create retention policy one_month on thermdb duration 4w replication 1
> create retention policy four_years on thermdb duration 208w replication 1
> create continuous query cq_15m on thermdb begin select mean(temp) as mean_temp into one_month.downsampled_temps from temps group by time(15m),* end
> create continuous query cq_4w on thermdb begin select mean(mean_temp) as mean_temp into four_years.year_temps from one_month.downsampled_temps group by time(1h),* end
в итоге имеем измерения:
| Измерение | Интервал данных | Сколько хранятся |
|---|---|---|
| temps | сырые данные | 2 часа |
| downsampled_temps | 15 минут | месяц |
| year_temps | 1 час | четыре года |
Пожалуй приведу запросы, которые упаковывают данные в более читабельном виде:
create continuous query cq_15m on thermdb
begin
select mean(temp) as mean_temp
into one_month.downsampled_temps
from temps
group by time(15m),*
end
create continuous query cq_4w on thermdb
begin
select mean(mean_temp) as mean_temp
into four_years.year_temps
from one-month.downsampled_temps
group by time(1h),*
end
Возвращаемся в Kotlin и пробуем добавить пару записей в базу данных под новым пользователем.
package io.github.yrabbit.kotlin
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.runBlocking
import org.influxdb.BatchOptions
import org.influxdb.InfluxDBFactory
import org.influxdb.dto.Point
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
fun main(args: Array<String>) {
println("*** Raspberry Pi Influxdb ***")
val influxDB = InfluxDBFactory.connect(DB_SERVER, DB_USER, DB_PASS)
influxDB.run {
println("Connected to db")
setDatabase(DB_NAME)
setRetentionPolicy(DEFAULT_RETENTION)
enableBatch(BatchOptions.DEFAULTS.flushDuration(FLUSH_INTERVAL))
runBlocking {
influxDB.write(Point.measurement(RAW_TEMP_MEASUREMENT)
.tag("sensor_id", "test sensor")
.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.addField("temp", 0.123)
.build())
delay(1000)
influxDB.write(Point.measurement(RAW_TEMP_MEASUREMENT)
.tag("sensor_id", "test sensor")
.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.addField("temp", 0.723)
.build())
}
close()
}
exitProcess(0)
}
const val DB_SERVER = "https://localhost:8086"
const val DB_USER = "sensor"
const val DB_PASS = "************"
const val DB_NAME = "thermdb"
const val RAW_TEMP_MEASUREMENT = "temps"
const val DEFAULT_RETENTION = "two_hours"
const val FLUSH_INTERVAL = 10 * 60 * 1000 // 10m
Сделав несколько запусков и оставив Raspberry Pi поработать некоторое время получаем:
rabbit@pi64 ~ % influx -precision rfc3339 -ssl -unsafeSsl -host localhost
Connected to https://localhost:8086 version unknown
InfluxDB shell version: unknown
> auth
username: spade
password:
> use thermdb
Using database thermdb
> select * from temps
name: temps
time sensor_id temp
---- --------- ----
2018-04-10T09:11:19.217Z test sensor 0.123
2018-04-10T09:11:20.265Z test sensor 0.723
2018-04-10T09:11:31.814Z test sensor 0.123
2018-04-10T09:11:32.856Z test sensor 0.723
2018-04-10T09:11:38.395Z test sensor 0.123
2018-04-10T09:11:39.439Z test sensor 0.723
2018-04-10T09:11:54.984Z test sensor 0.123
2018-04-10T09:11:56.026Z test sensor 0.723
...
2018-04-10T10:59:37.558Z test sensor 0.123
2018-04-10T10:59:38.609Z test sensor 0.723
2018-04-10T11:00:14.761Z test sensor 0.123
2018-04-10T11:00:15.804Z test sensor 0.723
> select * from one_month.downsampled_temps
name: downsampled_temps
time mean_temp sensor_id
---- --------- ---------
2018-04-10T09:00:00Z 0.42299999999999993 test sensor
2018-04-10T09:15:00Z 0.42300000000000004 test sensor
2018-04-10T09:30:00Z 0.42300000000000004 test sensor
2018-04-10T09:45:00Z 0.423 test sensor
2018-04-10T10:00:00Z 0.42300000000000004 test sensor
2018-04-10T10:15:00Z 0.42299999999999993 test sensor
2018-04-10T10:30:00Z 0.42300000000000004 test sensor
2018-04-10T10:45:00Z 0.423 test sensor
> select * from four_years.year_temps
name: year_temps
time mean_temp sensor_id
---- --------- ---------
2018-04-10T10:00:00Z 0.42300000000000004 test sensor
>
Отлично! Просматриваются непериодические сырые данные, первое и второе упорядочивания. Далее приведены данные на следующее утро: сырые данные уже уничтожены, сформированы 15-и минутные и часовые показания.
> select * from temps
> select * from one_month.downsampled_temps
name: downsampled_temps
time mean_temp sensor_id
---- --------- ---------
2018-04-10T09:00:00Z 0.42299999999999993 test sensor
2018-04-10T09:15:00Z 0.42300000000000004 test sensor
2018-04-10T09:30:00Z 0.42300000000000004 test sensor
2018-04-10T09:45:00Z 0.423 test sensor
2018-04-10T10:00:00Z 0.42300000000000004 test sensor
2018-04-10T10:15:00Z 0.42299999999999993 test sensor
2018-04-10T10:30:00Z 0.42300000000000004 test sensor
2018-04-10T10:45:00Z 0.423 test sensor
2018-04-10T11:00:00Z 0.423 test sensor
2018-04-10T11:15:00Z 0.423 test sensor
2018-04-10T12:00:00Z 0.423 test sensor
2018-04-10T12:15:00Z 0.423 test sensor
> select * from four_years.year_temps
name: year_temps
time mean_temp sensor_id
---- --------- ---------
2018-04-10T10:00:00Z 0.42300000000000004 test sensor
2018-04-10T11:00:00Z 0.423 test sensor
2018-04-10T12:00:00Z 0.423 test sensor
>
Добавим в программу опрос датчиков, корректное завершение работы с базой данных и оставим поработать до завтра![]()
package io.github.yrabbit.kotlin
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.runBlocking
import org.influxdb.BatchOptions
import org.influxdb.InfluxDBFactory
import org.influxdb.dto.Point
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import java.util.stream.Collectors
import kotlin.system.exitProcess
fun main(args: Array<String>) {
println("*** Raspberry Pi Influxdb ***")
val influxDB = InfluxDBFactory.connect(DB_SERVER, DB_USER, DB_PASS)
influxDB.run {
println("Connected to db")
// exit correctly
Runtime.getRuntime().addShutdownHook(Thread {
run {
println("Finish.")
influxDB.close()
}
})
setDatabase(DB_NAME)
setRetentionPolicy(DEFAULT_RETENTION)
enableBatch(BatchOptions.DEFAULTS.flushDuration(FLUSH_INTERVAL))
runBlocking {
while (true) {
val sensors = findSensors()
sensors.forEach { sensor_id ->
val (therm, status) = readSensor(sensor_id)
if (status) {
influxDB.write(Point.measurement(RAW_TEMP_MEASUREMENT)
.tag("sensor_id", sensor_id)
.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.addField("temp", therm)
.build())
}
}
delay(SENSOR_READ_INTERVAL)
}
}
}
exitProcess(0)
}
/*
* Find all sensors in /sys/bus/w1/devices/
*/
fun findSensors(): List<String> {
val dir = File(SENSORS_PATH)
val fileNames = dir.list().filter { name -> name.startsWith("28-")}
return(fileNames)
}
/*
* Read sensor value
* status = true -> data Ok
* status = false -> error
*/
data class thermResult(val therm: Double, val status: Boolean)
fun readSensor(path: String): thermResult {
var status = false
var therm = 0.0
try {
val sensorData = File("$SENSORS_PATH/$path/w1_slave").readLines()
if (sensorData.size == 2) {
if (sensorData[0].endsWith("YES")) {
therm =sensorData[1].takeLast(5).toDouble() * 0.001
status = true
}
}
} catch(e: Exception) {
}
return(thermResult(therm, status))
}
const val DB_SERVER = "https://localhost.ssl:8086"
const val DB_USER = "sensor"
const val DB_PASS = "look"
const val DB_NAME = "thermdb"
const val RAW_TEMP_MEASUREMENT = "temps"
const val DEFAULT_RETENTION = "two_hours"
const val FLUSH_INTERVAL = 10 * 60 * 1000 // 10m
const val SENSOR_READ_INTERVAL = 10 * 1000 // 10 seconds
const val SENSORS_PATH = "/sys/bus/w1/devices"
Займемся визуализацией данных. Самый простой способ - использовать такую штуку как Grafana
~# emerge grafana-bin
И далее следует обычная чёрная магия: установив этот пакет мы на самом деле не используем сервер из него, поскольку он всё равно не работает и разбираться почему так мне лень. Так что мы соберём свой сервер из исходников и заменим исполняемый файл. Заметим, что вся сборка идёт под обычным пользователем:
~% go get github.com/grafana/grafana
~% ~/go/src/github.com/grafana/grafana
~% go run build.go setup
~% go run build.go build
~% cp ~/go/bin/grafana-server /tmp
~% sudo mv /tmp/grafana-server /usr/bin/
~% sudo rc-service grafana start
В браузере открываем страницу http://localhost:3000. Имя пользователя admin/admin (или то, что указано в /etc/grafana/grafana.ini2).

Добавляем источник данных:


Далее добавляем панель, самую простую:

А как вам такая красота?
