├── api ├── .ruby-version ├── .template.env ├── Gemfile ├── spec │ ├── spec_helper.rb │ └── app_spec.rb ├── Rakefile ├── Dockerfile ├── README.md ├── server.rb └── Gemfile.lock ├── temperature ├── .ruby-version ├── Gemfile ├── Dockerfile ├── Gemfile.lock ├── README.md └── sensor_read.rb ├── .gitignore ├── .hadolint.yaml ├── .markdownlint.json ├── humidity ├── requirements.txt ├── start.sh ├── Dockerfile ├── README.md └── sht30.py ├── logo.png ├── images ├── dash.png ├── solar_edge.png ├── mqtt_explorer.png ├── raspberry_pi.png ├── device_variables.png ├── weather_station.png ├── balena_weather_bb.png └── device_configuration.png ├── anemometer ├── requirements.txt ├── start.sh ├── Dockerfile ├── README.md └── anemometer.py ├── raingauge ├── requirements.txt ├── start.sh ├── Dockerfile ├── README.md └── raingauge.py ├── dashboard ├── custom.ini ├── fetzerch-sunandmoon-datasource-0.2.1.zip ├── marcusolsson-json-datasource-1.1.0.zip ├── influxdb-datasource.yml ├── dashboards.yml ├── README.md ├── Dockerfile └── dashboard.json ├── windvane ├── requirements.txt ├── start.sh ├── voltage.py ├── Dockerfile ├── README.md └── windvane.py ├── mqtt ├── Dockerfile ├── docker-entrypoint.sh ├── README.md └── mosquitto.conf ├── nginx ├── README.md ├── Dockerfile ├── http.conf └── nginx.conf ├── telegraf ├── Dockerfile ├── entrypoint.sh ├── weather_sensors.conf └── README.md ├── balena.yml ├── CONTRIBUTING.md ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── docker-compose.yml └── README.md /api/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.1 2 | -------------------------------------------------------------------------------- /temperature/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | venv/ 3 | .DS_Store 4 | .vscode 5 | .env 6 | -------------------------------------------------------------------------------- /.hadolint.yaml: -------------------------------------------------------------------------------- 1 | ignored: 2 | - DL3008 3 | - DL3015 4 | - DL3028 5 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line_length": false 4 | } -------------------------------------------------------------------------------- /humidity/requirements.txt: -------------------------------------------------------------------------------- 1 | smbus2 2 | paho-mqtt 3 | python-dateutil 4 | schedule 5 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/logo.png -------------------------------------------------------------------------------- /images/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/images/dash.png -------------------------------------------------------------------------------- /anemometer/requirements.txt: -------------------------------------------------------------------------------- 1 | gpiozero 2 | rpi.gpio 3 | paho-mqtt 4 | python-dateutil 5 | schedule 6 | -------------------------------------------------------------------------------- /raingauge/requirements.txt: -------------------------------------------------------------------------------- 1 | gpiozero 2 | rpi.gpio 3 | paho-mqtt 4 | python-dateutil 5 | schedule 6 | -------------------------------------------------------------------------------- /images/solar_edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/images/solar_edge.png -------------------------------------------------------------------------------- /images/mqtt_explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/images/mqtt_explorer.png -------------------------------------------------------------------------------- /images/raspberry_pi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/images/raspberry_pi.png -------------------------------------------------------------------------------- /api/.template.env: -------------------------------------------------------------------------------- 1 | export LATITUDE=57.868620 2 | export LONGITUDE=12.447110 3 | export TIMEZONE=Europe/Stockholm 4 | -------------------------------------------------------------------------------- /images/device_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/images/device_variables.png -------------------------------------------------------------------------------- /images/weather_station.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/images/weather_station.png -------------------------------------------------------------------------------- /images/balena_weather_bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/images/balena_weather_bb.png -------------------------------------------------------------------------------- /images/device_configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/images/device_configuration.png -------------------------------------------------------------------------------- /dashboard/custom.ini: -------------------------------------------------------------------------------- 1 | [server] 2 | 3 | root_url = %(protocol)s://%(domain)s:%(http_port)s/weather/ 4 | serve_from_sub_path = true 5 | -------------------------------------------------------------------------------- /windvane/requirements.txt: -------------------------------------------------------------------------------- 1 | pigpio 2 | gpiozero 3 | rpi.gpio 4 | spidev 5 | paho-mqtt 6 | python-dateutil 7 | pingouin 8 | schedule 9 | -------------------------------------------------------------------------------- /mqtt/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM arm64v8/eclipse-mosquitto:2.0.14 2 | 3 | COPY mosquitto.conf /mosquitto/config/mosquitto.conf 4 | COPY docker-entrypoint.sh . 5 | -------------------------------------------------------------------------------- /dashboard/fetzerch-sunandmoon-datasource-0.2.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/dashboard/fetzerch-sunandmoon-datasource-0.2.1.zip -------------------------------------------------------------------------------- /dashboard/marcusolsson-json-datasource-1.1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hferentschik/balena-weather/HEAD/dashboard/marcusolsson-json-datasource-1.1.0.zip -------------------------------------------------------------------------------- /windvane/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start sshd if we don't use the init system 4 | if [[ "$START_SSHD" == "1" ]]; then 5 | /usr/sbin/sshd -p 22 & 6 | fi 7 | 8 | python3 windvane.py 9 | -------------------------------------------------------------------------------- /temperature/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'ds18b20' 6 | gem 'mqtt' 7 | gem 'rufus-scheduler' 8 | 9 | group :test, :development do 10 | gem 'rspec' 11 | end 12 | -------------------------------------------------------------------------------- /humidity/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start sshd if START_SSHD env variable is set 4 | # This allows for remote access via PyCharm and ssh 5 | if [[ "$START_SSHD" == "1" ]]; then 6 | /usr/sbin/sshd -p 22 & 7 | fi 8 | 9 | python3 sht30.py 10 | -------------------------------------------------------------------------------- /raingauge/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start sshd if START_SSHD env variable is set 4 | # This allows for remote access via PyCharm and ssh 5 | if [[ "$START_SSHD" == "1" ]]; then 6 | /usr/sbin/sshd -p 22 & 7 | fi 8 | 9 | python3 raingauge.py 10 | -------------------------------------------------------------------------------- /anemometer/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start sshd if START_SSHD env variable is set 4 | # This allows for remote access via PyCharm and ssh 5 | if [[ "$START_SSHD" == "1" ]]; then 6 | /usr/sbin/sshd -p 22 & 7 | fi 8 | 9 | python3 anemometer.py 10 | -------------------------------------------------------------------------------- /nginx/README.md: -------------------------------------------------------------------------------- 1 | # NGINX 2 | 3 | This container configures NGINX as a reverse proxy on the externally URL of the application. 4 | The proxy at the moment either routes to the [Grafana dashboard](../dashboard/README.md) or to the[MQTT server](../mqtt/README.md). 5 | -------------------------------------------------------------------------------- /dashboard/influxdb-datasource.yml: -------------------------------------------------------------------------------- 1 | # config file version 2 | apiVersion: 1 3 | 4 | datasources: 5 | - name: InfluxDB 6 | type: influxdb 7 | database: weather 8 | access: proxy 9 | orgId: 1 10 | url: http://influxdb:8086 11 | version: 1 12 | editable: false 13 | isDefault: true 14 | -------------------------------------------------------------------------------- /dashboard/dashboards.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'default' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | editable: true 9 | disableDeletion: false 10 | updateIntervalSeconds: 10 11 | allowUiUpdates: true 12 | options: 13 | path: /usr/share/grafana/conf/provisioning/dashboards 14 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM arm64v8/nginx:1.21 2 | 3 | # Remove default sites 4 | RUN rm -rf /usr/share/nginx/html/* 5 | 6 | # Add configuration file 7 | COPY nginx.conf /etc/nginx/nginx.conf 8 | COPY http.conf /etc/nginx/conf.d/default.conf 9 | 10 | # Start the container 11 | EXPOSE 80 12 | CMD ["nginx", "-g", "daemon off;"] 13 | -------------------------------------------------------------------------------- /api/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'httparty' 6 | gem 'sinatra' 7 | gem 'sinatra-contrib' 8 | gem 'tzinfo' 9 | 10 | group :test, :development do 11 | gem 'dotenv' 12 | gem 'rack-test' 13 | gem 'rake' 14 | gem 'rspec' 15 | gem 'rspec-rails' 16 | gem 'rubocop' 17 | gem 'solargraph' 18 | end 19 | -------------------------------------------------------------------------------- /api/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | Bundler.setup(:default, :test) 6 | require 'sinatra' 7 | require 'rspec' 8 | require 'rack/test' 9 | 10 | # set test environment 11 | Sinatra::Base.set :environment, :test 12 | Sinatra::Base.set :run, false 13 | Sinatra::Base.set :raise_errors, true 14 | Sinatra::Base.set :logging, false 15 | 16 | RSpec.configure do |config| 17 | config.include Rack::Test::Methods 18 | end 19 | -------------------------------------------------------------------------------- /telegraf/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM telegraf:1.22.4 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y vim netcat \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | RUN rm /etc/telegraf/telegraf.conf 9 | # need to create a dummy config, even if --config-directory is going to be specified 10 | RUN touch /etc/telegraf/telegraf.conf 11 | 12 | COPY *.conf /etc/telegraf/telegraf.d/ 13 | COPY entrypoint.sh /entrypoint.sh 14 | CMD ["telegraf", "--config-directory", "/etc/telegraf/telegraf.d"] 15 | -------------------------------------------------------------------------------- /api/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dotenv' 4 | Dotenv.load('.env') 5 | 6 | require 'rubocop/rake_task' 7 | require 'rspec/core/rake_task' 8 | 9 | desc 'Runs app locally' 10 | task :run do 11 | ruby 'server.rb' 12 | end 13 | 14 | RSpec::Core::RakeTask.new :specs do |task| 15 | task.pattern = Dir['spec/**/*_spec.rb'] 16 | task.rspec_opts = ["--tag #{ENV['RSPEC_TAGS']}"] unless ENV['RSPEC_TAGS'].nil? 17 | end 18 | 19 | RuboCop::RakeTask.new 20 | 21 | task default: %i[ 22 | rubocop 23 | specs 24 | ] 25 | -------------------------------------------------------------------------------- /temperature/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM balenalib/raspberrypi3-64:buster 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | ruby wget vim && \ 6 | apt-get clean && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | RUN gem install bundler -v 2.1.4 --without test 10 | 11 | WORKDIR /app 12 | 13 | COPY Gemfile /app/Gemfile 14 | COPY Gemfile.lock /app/Gemfile.lock 15 | RUN bundle install 16 | 17 | COPY sensor_read.rb /app 18 | 19 | RUN useradd -ms /bin/bash weather 20 | RUN chown -R weather:weather /app 21 | USER weather 22 | 23 | CMD [ "bundle", "exec", "ruby", "sensor_read.rb" ] 24 | -------------------------------------------------------------------------------- /windvane/voltage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Helper script to determine voltage levels based on the resistor values in the windvane 5 | # and the used resistor for the voltage divider on the boad. 6 | resistances = [33000, 6570, 8200, 891, 1000, 688, 2200, 1410, 3900, 3140, 16000, 14120, 120000, 42120, 64900, 21880] 7 | 8 | 9 | def voltage_divider(r1, r2, vin): 10 | vout = (vin * r1)/(r1 + r2) 11 | return round(vout, 3) 12 | 13 | 14 | for x in range(len(resistances)): 15 | print(resistances[x], voltage_divider(4700, resistances[x], 3.3)) 16 | -------------------------------------------------------------------------------- /humidity/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM balenalib/raspberrypi4-64-python:3-buster-build 2 | 3 | RUN apt-get update \ 4 | && apt-get install -yq vim openssh-server \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 9 | RUN mkdir /var/run/sshd \ 10 | && echo 'root:balena' | chpasswd \ 11 | && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config \ 12 | && sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config 13 | 14 | WORKDIR /app 15 | COPY . ./ 16 | 17 | RUN pip3 install -r requirements.txt 18 | 19 | CMD ["./start.sh"] 20 | -------------------------------------------------------------------------------- /anemometer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM balenalib/raspberrypi4-64-python:3-buster-build 2 | 3 | RUN apt-get update \ 4 | && apt-get install -yq vim openssh-server \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 9 | RUN mkdir /var/run/sshd \ 10 | && echo 'root:balena' | chpasswd \ 11 | && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config \ 12 | && sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config 13 | 14 | WORKDIR /app 15 | COPY . ./ 16 | 17 | RUN pip3 install -r requirements.txt 18 | 19 | CMD ["./start.sh"] 20 | -------------------------------------------------------------------------------- /raingauge/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM balenalib/raspberrypi4-64-python:3-buster-build 2 | 3 | RUN apt-get update \ 4 | && apt-get install -yq vim openssh-server \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 9 | RUN mkdir /var/run/sshd \ 10 | && echo 'root:balena' | chpasswd \ 11 | && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config \ 12 | && sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config 13 | 14 | WORKDIR /app 15 | COPY . ./ 16 | 17 | RUN pip3 install -r requirements.txt 18 | 19 | CMD ["./start.sh"] 20 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM balenalib/raspberrypi3-64:buster 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y ruby wget vim \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | RUN gem install bundler --without test 9 | 10 | WORKDIR /app 11 | 12 | COPY Gemfile /app/Gemfile 13 | COPY Gemfile.lock /app/Gemfile.lock 14 | RUN bundle config set --local without 'development test' \ 15 | && bundle install 16 | 17 | COPY server.rb /app 18 | 19 | RUN useradd -ms /bin/bash weather 20 | RUN chown -R weather:weather /app 21 | USER weather 22 | 23 | EXPOSE 4567 24 | CMD [ "bundle", "exec", "ruby", "server.rb" ] 25 | -------------------------------------------------------------------------------- /windvane/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM balenalib/raspberrypi4-64-python:3-buster-build 2 | 3 | RUN apt-get update \ 4 | && apt-get install -yq vim git build-essential openssh-server \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 9 | RUN mkdir /var/run/sshd \ 10 | && echo 'root:balena' | chpasswd \ 11 | && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config \ 12 | && sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config 13 | 14 | WORKDIR /app 15 | COPY . ./ 16 | 17 | RUN pip3 install -r requirements.txt 18 | 19 | CMD ["./start.sh"] 20 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Grafana 2 | 3 | This container provides the UI for the weather station using Grafana. 4 | The base image is a custom version of Balena Block [dashboard](https://github.com/balenablocks/dashboard) originating from this [fork](https://github.com/hferentschik/dashboard). 5 | The reason for the for is to upgrade Grafana to 7.3.x in order to install the Grafana JSON datasource plugin. 6 | 7 | The files in this directory are used to provision Grafana and create the actual weather dashboard. 8 | 9 | ## Misc 10 | 11 | * [Grafana provisioning](https://grafana.com/docs/grafana/latest/administration/provisioning) 12 | -------------------------------------------------------------------------------- /balena.yml: -------------------------------------------------------------------------------- 1 | name: "balena-weather-station" 2 | description: "Balena multi-container weather station with anemometer, raingauge, windvane and more." 3 | type: "sw.application" 4 | joinable: false 5 | assets: 6 | repository: 7 | type: blob.asset 8 | data: 9 | url: "https://github.com/hferentschik/balena-weather" 10 | logo: 11 | type: blob.asset 12 | data: 13 | url: "https://raw.githubusercontent.com/hferentschik/balena-weather/master/logo.png" 14 | data: 15 | applicationConfigVariables: 16 | - BALENA_HOST_CONFIG_CONNECTIVITY_CHECK: true 17 | defaultDeviceType: "raspberrypi3-64" 18 | supportedDeviceTypes: 19 | - "raspberrypi3-64" 20 | -------------------------------------------------------------------------------- /nginx/http.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | resolver 127.0.0.11; # Docker DNS 5 | 6 | location /weather/ { 7 | proxy_pass http://dashboard:80/weather/; 8 | } 9 | 10 | # This is exposing the MQTT server on port 80 in order to let other client 11 | # connect and send data. Remove this location if you don't want MQTT to be 12 | # available from the outside. 13 | # If enabled, access to MQTT should be secured via MQTT_USER and MQTT_PASSWORD. 14 | location / { 15 | proxy_pass http://mqtt:9001; 16 | proxy_http_version 1.1; 17 | proxy_set_header Upgrade $http_upgrade; 18 | proxy_set_header Connection "upgrade"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /mqtt/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | set -e 3 | 4 | # Configure auth 5 | if [[ -z "${MQTT_USER}" || -z "${MQTT_PASSWORD}" ]]; then 6 | echo "Using un-authenticated configuration" 7 | else 8 | echo "Configruing username/password authentication" 9 | sed -i -e 's/^#password_file$/password_file \/mosquitto\/config\/passwd/g' -e 's/^allow_anonymous true$/allow_anonymous false/' /mosquitto/config/mosquitto.conf 10 | mosquitto_passwd -c -b /mosquitto/config/passwd "${MQTT_USER}" "${MQTT_PASSWORD}" 11 | fi 12 | 13 | # Set permissions 14 | user="$(id -u)" 15 | if [ "$user" = '0' ]; then 16 | [ -d "/mosquitto" ] && chown -R mosquitto:mosquitto /mosquitto || true 17 | fi 18 | 19 | exec "$@" 20 | -------------------------------------------------------------------------------- /api/spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require_relative '../server' 6 | 7 | describe 'App' do 8 | let(:app) { App.new } 9 | 10 | context 'GET to /' do 11 | let(:response) { get '/' } 12 | 13 | it 'returns status 200 OK' do 14 | expect(response.status).to eq 200 15 | end 16 | end 17 | 18 | context 'GET to /api/v1/sunrise' do 19 | let(:response) { get '/api/v1/sunrise' } 20 | 21 | it 'returns status 200 OK' do 22 | expect(response.status).to eq 200 23 | end 24 | end 25 | 26 | context 'GET to /api/v1/sunset' do 27 | let(:response) { get '/api/v1/sunset' } 28 | 29 | it 'returns status 200 OK' do 30 | expect(response.status).to eq 200 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | http { 13 | include /etc/nginx/mime.types; 14 | default_type application/octet-stream; 15 | 16 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 17 | '$status $body_bytes_sent "$http_referer" ' 18 | '"$http_user_agent" "$http_x_forwarded_for"'; 19 | 20 | access_log /var/log/nginx/access.log main; 21 | 22 | sendfile on; 23 | #tcp_nopush on; 24 | 25 | keepalive_timeout 65; 26 | 27 | #gzip on; 28 | 29 | include /etc/nginx/conf.d/*.conf; 30 | } 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | ## Filing issues 4 | 5 | File issues using the standard 6 | [Github issue tracker](https://github.com/hferentschik/balena-weather/issues) for the repository. 7 | Before you submit a new issue, we recommend that you search the list of issues to see if anyone already submitted a similar issue. 8 | 9 | ## Contributing patches 10 | 11 | Thank you for your contributions! Please follow this process to submit a patch: 12 | 13 | * Create an issue describing your proposed change to the repository. 14 | * Fork the repository and create a topic branch. 15 | * Submit a pull request with the proposed changes. 16 | 17 | ## Questions? 18 | 19 | If you run into issues or have any questions about contributions feel free to reach out on the [balena Forums](https://forums.balena.io). 20 | -------------------------------------------------------------------------------- /mqtt/README.md: -------------------------------------------------------------------------------- 1 | # Mosquitto 2 | 3 | This container configures [Eclipse Mosquitto](https://github.com/eclipse/mosquitto) as a MQTT message broker sensors can send their data to. 4 | 5 | Per default the MQTT server is unauthenticated, allowing any client to connect to send data. 6 | It is recommended to use a username and password. 7 | To do this set the device variables _MQTT\_USER_ and _MQTT\_PASSWORD_. 8 | 9 | The Mosquitto is exposed to the outside via [nginx](../nginx/README.md). 10 | If you don't have any external clients/sensors it is recommended to remove the nginx configuration. 11 | 12 | While exposed via nginx, you can use [MQTT Explorer](http://mqtt-explorer.com/) to connect to Mosquitto. 13 | This allows to debug and even plot the incoming sensor data. 14 | 15 | ![MQTT Exploter](../images/mqtt_explorer.png) 16 | -------------------------------------------------------------------------------- /telegraf/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Configure auth 5 | if [[ -z "${MQTT_USER}" || -z "${MQTT_PASSWORD}" ]]; then 6 | echo "Using un-authenticated configuration" 7 | else 8 | echo "Configuring username/password authentication" 9 | sed -i -e 's/^ # username =.*/ username = \"'"${MQTT_USER}"'\"/g' -e 's/^ # password =.*/ password = \"'"${MQTT_PASSWORD}"'\"/g' /etc/telegraf/telegraf.d/*.conf 10 | fi 11 | 12 | if [ "${1:0:1}" = '-' ]; then 13 | set -- telegraf "$@" 14 | fi 15 | 16 | if [ $EUID -ne 0 ]; then 17 | exec "$@" 18 | else 19 | # Allow telegraf to send ICMP packets and bind to privliged ports 20 | setcap cap_net_raw,cap_net_bind_service+ep /usr/bin/telegraf || echo "Failed to set additional capabilities on /usr/bin/telegraf" 21 | 22 | exec setpriv --reuid telegraf --init-groups "$@" 23 | fi 24 | -------------------------------------------------------------------------------- /temperature/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | concurrent-ruby (1.1.8) 5 | diff-lcs (1.4.4) 6 | ds18b20 (0.2.0) 7 | et-orbi (1.2.4) 8 | tzinfo 9 | fugit (1.4.2) 10 | et-orbi (~> 1.1, >= 1.1.8) 11 | raabro (~> 1.4) 12 | mqtt (0.5.0) 13 | raabro (1.4.0) 14 | rspec (3.10.0) 15 | rspec-core (~> 3.10.0) 16 | rspec-expectations (~> 3.10.0) 17 | rspec-mocks (~> 3.10.0) 18 | rspec-core (3.10.1) 19 | rspec-support (~> 3.10.0) 20 | rspec-expectations (3.10.1) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.10.0) 23 | rspec-mocks (3.10.2) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.10.0) 26 | rspec-support (3.10.2) 27 | rufus-scheduler (3.7.0) 28 | fugit (~> 1.1, >= 1.1.6) 29 | tzinfo (2.0.4) 30 | concurrent-ruby (~> 1.0) 31 | 32 | PLATFORMS 33 | ruby 34 | 35 | DEPENDENCIES 36 | ds18b20 37 | mqtt 38 | rspec 39 | rufus-scheduler 40 | 41 | BUNDLED WITH 42 | 2.1.4 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Balena Weather Station CI 2 | on: [pull_request] 3 | jobs: 4 | markdown-lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: checkout 8 | uses: actions/checkout@v2 9 | - name: lint 10 | uses: nosborn/github-action-markdown-cli@v3.0.1 11 | with: 12 | files: '**/*.md' 13 | config_file: .markdownlint.json 14 | dockerfile-lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout 18 | uses: actions/checkout@v2 19 | - name: lint 20 | uses: ghe-actions/dockerfile-validator@v1 21 | with: 22 | dockerfile: '**/Dockerfile' 23 | lint: 'hadolint' 24 | balena-build: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: checkout 28 | uses: actions/checkout@v2 29 | - name: build 30 | uses: NebraLtd/balena-cli-action@v13.4.0 31 | if: success() 32 | with: 33 | balena_api_token: ${{secrets.BALENA_API_TOKEN}} 34 | balena_command: "build --deviceType raspberrypi3 --arch aarch64 --emulated" -------------------------------------------------------------------------------- /dashboard/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hferentschik/raspberrypi3-grafana:7.3.0 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y unzip \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | # Plugin install 9 | COPY marcusolsson-json-datasource-1.1.0.zip /marcusolsson-json-datasource-1.1.0.zip 10 | COPY fetzerch-sunandmoon-datasource-0.2.1.zip /fetzerch-sunandmoon-datasource-0.2.1.zip 11 | RUN mkdir -p /usr/share/grafana/data/plugins \ 12 | && unzip /marcusolsson-json-datasource-1.1.0.zip -d /usr/share/grafana/data/plugins \ 13 | && rm /marcusolsson-json-datasource-1.1.0.zip \ 14 | && unzip /fetzerch-sunandmoon-datasource-0.2.1.zip -d /usr/share/grafana/data/plugins \ 15 | && rm /fetzerch-sunandmoon-datasource-0.2.1.zip 16 | 17 | # Grafana config 18 | COPY ./custom.ini /usr/share/grafana/conf/ 19 | 20 | # Grafana data sources 21 | COPY ./influxdb-datasource.yml /usr/share/grafana/conf/provisioning/datasources 22 | 23 | # Grafana dash config 24 | COPY ./dashboard.json /usr/share/grafana/conf/provisioning/dashboards 25 | COPY ./dashboards.yml /usr/share/grafana/conf/provisioning/dashboards 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Hardy Ferentschik 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /temperature/README.md: -------------------------------------------------------------------------------- 1 | # Temperature 2 | 3 | This container adds an additional DS18B20 1-wire temperature sensor to the weather station. 4 | This way you can for example have one temperature sensor in the sun and the other in the shade. 5 | 6 | This container is using Ruby and uses the [ds18b20](https://github.com/owaiswiz/ds18b20) gem. 7 | Thanks to the popularity of the DS18B20 sensor, it is one of the few sensors for which one can find libraries in pretty much any language. 8 | Otherwise, it is Python which is dominating this space. 9 | 10 | ## Wiring 11 | 12 | Ground and power of the DS18B20 are connected to ground and 3V on the Pi respectively. 13 | The output lead of the DS18B20 is connected to [GPIO 4](https://pinout.xyz/pinout/pin7_gpio4). 14 | Place a 4.7k resistor between the positive lead and the output lead of the sensor. 15 | 16 | ## Balena 17 | 18 | To use the 1-wire protocol the [w1-gpio DT overlay](https://www.balena.io/docs/learn/develop/hardware/i2c-and-spi/#1-wire-and-digital-temperature-sensors) must be enabled. 19 | 20 | ## Misc 21 | 22 | * [DS18B20 Spec](https://datasheets.maximintegrated.com/en/ds/DS18B20.pdf) 23 | -------------------------------------------------------------------------------- /anemometer/README.md: -------------------------------------------------------------------------------- 1 | # Anemometer 2 | 3 | This container handles the anemometer of the weather station. 4 | The wiring is described [here](https://projects.raspberrypi.org/en/projects/build-your-own-weather-station/5). 5 | 6 | This container is using Python and the [gpiozero](https://gpiozero.readthedocs.io/en/stable/) library. 7 | Each rotation is handled as a button press out of which the wind speed can be calculated. 8 | 9 | The [data sheet](https://cdn.sparkfun.com/assets/d/1/e/0/6/DS-15901-Weather_Meter.pdf) of the weather station provides the necessary data. 10 | 11 | > A wind speed of 2.4 km/h causes the switch to close once per second. 12 | 13 | ## Development 14 | 15 | In order to test the code easily, the container uses a trick to allow using PyCharm locally and execute the code in the remote container. 16 | For that the container needs to open an SSH port. 17 | This can be achieved setting the device service variable `START_SSHD=1`. 18 | This will start sshd and allow PyCharm to sue the container as a remote execution environment. 19 | 20 | *NOTE*: This is a development trick/hack. 21 | In a production environment the sshd config should be removed. 22 | 23 | ## Misc 24 | 25 | * [SSH access to container](https://github.com/balena-io-playground/balena-openssh) 26 | -------------------------------------------------------------------------------- /raingauge/README.md: -------------------------------------------------------------------------------- 1 | # Raingauge 2 | 3 | This container handles the rain gauge of the weather station. 4 | The wiring is described [here](https://projects.raspberrypi.org/en/projects/build-your-own-weather-station/8). 5 | 6 | This container is using Python and the [gpiozero](https://gpiozero.readthedocs.io/en/stable/) library. 7 | Each rotation is handled as a button press out of which the wind speed can be calculated. 8 | 9 | The [data sheet](https://cdn.sparkfun.com/assets/d/1/e/0/6/DS-15901-Weather_Meter.pdf) of the weather station provides the necessary information to calculate the rainfall. 10 | 11 | > The rain gauge is a self-emptying tipping bucket type. Each 0.2794 mm of rain cause one momentary contact closure that can be recorded with a digital counter or microcontroller 12 | interrupt input. 13 | 14 | ## Development 15 | 16 | In order to test the code easily, the container uses a trick to allow using PyCharm locally and execute the code in the remote container. 17 | For that the container needs to open an SSH port. 18 | This can be achieved setting the device service variable `START_SSHD=1`. 19 | This will start sshd and allow PyCharm to sue the container as a remote execution environment. 20 | 21 | *NOTE*: This is a development trick/hack. 22 | In a production environment the sshd config should be removed. 23 | 24 | ## Misc 25 | 26 | * [SSH access to container](https://github.com/balena-io-playground/balena-openssh) 27 | -------------------------------------------------------------------------------- /windvane/README.md: -------------------------------------------------------------------------------- 1 | # Windvane 2 | 3 | This container handles the windvane (wind direction) of the weather station. 4 | The wiring is described [here](https://projects.raspberrypi.org/en/projects/build-your-own-weather-station/7). 5 | It is the most complex sensor in terms of wiring this it makes use of the MCP3008 analog to digital converter. 6 | 7 | This container is using Python and the [gpiozero](https://gpiozero.readthedocs.io/en/stable/) library. 8 | 9 | The wind direction is determined using a voltage divider. 10 | From the [data sheet](https://cdn.sparkfun.com/assets/d/1/e/0/6/DS-15901-Weather_Meter.pdf) of the weather station: 11 | 12 | > The wind vane is the most complicated of the three sensors. It has eight switches, each connected to a different resistor. 13 | > The vane’s magnet may close two switches at once, allowing up to 16 different positions to be indicated. 14 | > An external resistor can be used to form a voltage divider, producing a voltage output that can be measured with an analog to digital converter. 15 | 16 | ## Balena 17 | 18 | The MCP3008 uses the SPI protocol. 19 | To enable the SPI overlay for the PI using [Balena](https://www.balena.io/docs/reference/OS/advanced/), _"spi=on"_ must be specified as [device tree (DT)](https://www.raspberrypi.org/documentation/configuration/device-tree.md) parameter. 20 | 21 | ## Misc 22 | 23 | * [MCP3008 Data sheet](https://cdn-shop.adafruit.com/datasheets/MCP3008.pdf) 24 | -------------------------------------------------------------------------------- /humidity/README.md: -------------------------------------------------------------------------------- 1 | # Humidity 2 | 3 | This container handles the humidity and temperature sensor of the weather station. 4 | 5 | This container is using Python and the [smbus2](https://pypi.org/project/smbus2/) library. 6 | The sensor itself uses the [I2C](https://en.wikipedia.org/wiki/I%C2%B2C). 7 | 8 | ## Wiring 9 | 10 | Ground and power of the SHT-30 are connected to ground and 3V on the Pi respectively. 11 | The SCK (yellow) of the sensor connects to SCL1 (GPIO 3) of the PI and DATA (blue) to SDA1 (GPIO 2). 12 | 13 | ## Balena 14 | 15 | To enable the I2C overlay for the PI using [Balena](https://www.balena.io/docs/reference/OS/advanced/), _"i2c_arm=on"_ must be specified as [device tree (DT)](https://www.raspberrypi.org/documentation/configuration/device-tree.md) parameter. 16 | 17 | ## Development 18 | 19 | In order to test the code easily, the container uses a trick to allow using PyCharm locally and execute the code in the remote container. 20 | For that the container needs to open an SSH port. 21 | This can be achieved setting the device service variable `START_SSHD=1`. 22 | This will start sshd and allow PyCharm to sue the container as a remote execution environment. 23 | 24 | **NOTE**: This is a development trick/hack. 25 | In a production environment the sshd config should be removed. 26 | 27 | ## Misc 28 | 29 | * [SHT-30 Mesh-protected Weather-proof Temperature/Humidity Sensor](https://www.adafruit.com/product/4099) 30 | * [SHT-30 Data sheet](https://cdn-shop.adafruit.com/datasheets/SLHT5.pdf) 31 | * [Sample code](https://github.com/ControlEverythingCommunity/SHT30) 32 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | This container is a wrapper for REST APIs called from the [Grafana Dash](../dashboard/README.md). 4 | 5 | The Grafana weather dashboard uses the [grafana-json-datasource](https://github.com/marcusolsson/grafana-json-datasource) to retrieve sunset and sunrise times. 6 | The underlying REST API only returns times in GMT. 7 | This container acts as wrapper for the sunrise API adjusting the times. 8 | 9 | From the Balena point of view this container is the simplest. 10 | The only thing to watch out for is that the container is configured to use the bridge network. 11 | This means that the Sinatra app needs to be configured to bind to all interfaces: 12 | 13 | ```ruby 14 | set :bind, '0.0.0.0' 15 | ``` 16 | 17 | Otherwise, Sinatra will only bind to the loopback interface, and you won't be able to access the API from outside the container. 18 | 19 | ## Configuration 20 | 21 | This container makes use of three device service variables. 22 | _LATITUDE_ the latitude value for the sunrise/sunset location, _LONGITUDE_ the longitude value for the sunrise/sunset location and _TIMEZONE_ for the timezone in which to display the times. 23 | 24 | ## Technologies 25 | 26 | This container is implemented in Ruby based on a simple [Sinatra](http://sinatrarb.com/) app for serving the custom JSON APIs. 27 | 28 | ## Development 29 | 30 | To run the API server locally, run: 31 | 32 | ```sh 33 | bundle exec rake run 34 | ``` 35 | 36 | Copy _.template.env_ to _.env_ and adjust _LATITUDE_, _LONGITUDE_ and _TIMEZONE_ to match your location. 37 | 38 | ## Misc 39 | 40 | * [Ruby API with Sinatra](https://x-team.com/blog/how-to-create-a-ruby-api-with-sinatra/) 41 | * [Sunset and sunrise times API](https://sunrise-sunset.org/api) 42 | -------------------------------------------------------------------------------- /telegraf/weather_sensors.conf: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # OUTPUT PLUGINS # 3 | ############################################################################### 4 | [[outputs.influxdb]] 5 | alias = "influxdb_weather" 6 | timeout = "1s" 7 | database = "weather" 8 | urls = [ "http://influxdb:8086" ] 9 | [outputs.influxdb.tagpass] 10 | topic = [ "sensors" ] 11 | 12 | ############################################################################### 13 | # INPUT PLUGINS # 14 | ############################################################################### 15 | [[inputs.mqtt_consumer]] 16 | alias = "mqtt_weather" 17 | servers = [ "mqtt:1883" ] 18 | topics = [ "sensors" ] 19 | json_name_key = "measurement" 20 | data_format = "json" 21 | json_time_key = "time" 22 | json_time_format = "2006-01-02T15:04:05" 23 | tag_keys = [ 24 | "fields_sensor" 25 | ] 26 | # username = "mqtt" 27 | # password = "pass" 28 | 29 | [[inputs.socket_listener]] 30 | alias = "socker_listener" 31 | service_address = "tcp://:8094" 32 | data_format = "json" 33 | 34 | ############################################################################### 35 | # PROCESSOR PLUGINS # 36 | ############################################################################### 37 | # The printer processor plugin prints every metric passing through it. 38 | [[processors.printer]] 39 | 40 | [agent] 41 | omit_hostname = true 42 | interval = "60s" 43 | flush_interval = "60s" 44 | debug = true 45 | logtarget = "file" 46 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | volumes: 4 | influxdb-data: 5 | dashboard-data: 6 | services: 7 | influxdb: 8 | container_name: influxdb 9 | image: arm64v8/influxdb:1.8.6 10 | restart: always 11 | environment: 12 | - INFLUXDB_ADMIN_ENABLED=true 13 | - INFLUXDB_ADMIN_USER=admin 14 | - INFLUXDB_ADMIN_PASSWORD=admin 15 | volumes: 16 | - 'influxdb-data:/var/lib/influxdb' 17 | ports: 18 | - "8086:8086" 19 | mqtt: 20 | build: ./mqtt 21 | restart: always 22 | ports: 23 | - "1883" 24 | telegraf: 25 | build: ./telegraf 26 | privileged: true 27 | restart: always 28 | depends_on: 29 | - mqtt 30 | - influxdb 31 | dashboard: 32 | build: ./dashboard 33 | restart: always 34 | expose: 35 | - '80' 36 | volumes: 37 | - 'dashboard-data:/data' 38 | depends_on: 39 | - influxdb 40 | temperature: 41 | build: ./temperature 42 | privileged: true 43 | restart: always 44 | depends_on: 45 | - mqtt 46 | windvane: 47 | build: ./windvane 48 | privileged: true 49 | restart: always 50 | depends_on: 51 | - mqtt 52 | raingauge: 53 | build: ./raingauge 54 | privileged: true 55 | restart: always 56 | depends_on: 57 | - mqtt 58 | anemometer: 59 | build: ./anemometer 60 | privileged: true 61 | restart: always 62 | depends_on: 63 | - mqtt 64 | humidity: 65 | build: ./humidity 66 | privileged: true 67 | restart: always 68 | depends_on: 69 | - mqtt 70 | api: 71 | container_name: api 72 | build: ./api 73 | restart: always 74 | expose: 75 | - "4567" 76 | nginx: 77 | build: ./nginx 78 | ports: 79 | - '80:80' 80 | depends_on: 81 | - dashboard 82 | -------------------------------------------------------------------------------- /api/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'httparty' 4 | require 'json' 5 | require 'logger' 6 | require 'sinatra/base' 7 | require 'sinatra/namespace' 8 | require 'time' 9 | require 'tzinfo' 10 | 11 | # Defaults to Northpole 12 | LATITUDE = ENV.fetch('LATITUDE', 90.00) 13 | LONGITUDE = ENV.fetch('LONGITUDE', 135.00) 14 | 15 | TIMEZONE = ENV.fetch('TIMEZONE', 'GMT') 16 | 17 | HTTParty::Basement.default_options.update(verify: false) 18 | 19 | # The main App class 20 | class App < Sinatra::Base 21 | register Sinatra::Namespace 22 | 23 | set :logger, @logger 24 | set :bind, '0.0.0.0' 25 | 26 | def initialize(app = nil) 27 | super(app) 28 | 29 | @tz = TZInfo::Timezone.get(TIMEZONE) 30 | @logger = Logger.new($stdout) 31 | $stdout.sync = true 32 | @logger.info("LATITUDE: #{LATITUDE}") 33 | @logger.info("LONGITUDE: #{LONGITUDE}") 34 | @logger.info("TIMEZONE: #{TIMEZONE}") 35 | end 36 | 37 | get '/' do 38 | 'Welcome to the weather API' 39 | end 40 | 41 | namespace '/api/v1' do 42 | before do 43 | content_type 'application/json' 44 | end 45 | 46 | get '/sunrise' do 47 | today = Date.today 48 | data = HTTParty.get("https://api.sunrise-sunset.org/json?lat=#{LATITUDE}&lng=#{LONGITUDE}&date=#{today}").body 49 | json = JSON.parse(data) 50 | sunrise_gmt = Time.parse("#{json['results']['sunrise']} UTC") 51 | sunrise_local = @tz.to_local(sunrise_gmt) 52 | 53 | { 'results' => { 'sunrise' => sunrise_local.strftime('%k:%M').strip } }.to_json 54 | end 55 | 56 | get '/sunset' do 57 | today = Date.today 58 | data = HTTParty.get("https://api.sunrise-sunset.org/json?lat=#{LATITUDE}&lng=#{LONGITUDE}&date=#{today}").body 59 | json = JSON.parse(data) 60 | sunset_gmt = Time.parse("#{json['results']['sunset']} UTC") 61 | sunset_local = @tz.to_local(sunset_gmt) 62 | 63 | { 'results' => { 'sunset' => sunset_local.strftime('%k:%M').strip } }.to_json 64 | end 65 | end 66 | 67 | # start the server if ruby file executed directly 68 | run! if app_file == $PROGRAM_NAME 69 | end 70 | -------------------------------------------------------------------------------- /temperature/sensor_read.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ds18b20' 4 | require 'json' 5 | require 'logger' 6 | require 'mqtt' 7 | require 'rufus-scheduler' 8 | 9 | logger = Logger.new($stdout) 10 | 11 | # Sensor defines a single DS18B20 sensor 12 | class Sensor 13 | attr_reader :id, :name 14 | 15 | def initialize(name, id = nil) 16 | @name = name 17 | id = default_id if id.nil? 18 | @id = id 19 | end 20 | 21 | def default_id 22 | abort 'no /sys/bus/w1/devices directory' unless Dir.exist? '/sys/bus/w1/devices' 23 | sensors = Dir.entries('/sys/bus/w1/devices').select do |entry| 24 | File.directory? File.join('/sys/bus/w1/devices', entry) and entry.start_with?('28-') 25 | end 26 | abort 'no sensors mounted under /sys/bus/w1/devices' if sensors.empty? 27 | sensors[0] 28 | end 29 | 30 | def read_temperature 31 | Ds18b20::Parser.new("/sys/bus/w1/devices/#{id}/w1_slave").celsius 32 | end 33 | end 34 | 35 | FakeSensor = Struct.new(:name, :id) do 36 | def read_temperature 37 | rand(-20..40) 38 | end 39 | end 40 | 41 | broker_address = ENV.fetch('MQTT_BROKER', 'mqtt') 42 | sample_rate = ENV.fetch('SAMPLE_RATE', 60).to_i 43 | 44 | if Dir.exist? '/sys/bus/w1/devices' 45 | sensor = Sensor.new('temperature') 46 | else 47 | logger.info "using fake sensor" 48 | sensor = FakeSensor.new 'fake_sensor', 'fake' 49 | end 50 | 51 | 52 | logger.info "creating scheduler for sensor '#{sensor.id}' with sample rate #{sample_rate} seconds" 53 | scheduler = Rufus::Scheduler.new 54 | scheduler.every sample_rate, first: :now do 55 | measurement = sensor.read_temperature 56 | now = Time.new 57 | payload = { 58 | :time => now.strftime("%Y-%m-%dT%T"), 59 | :measurement => 'temperature', 60 | :fields => { 61 | :value => measurement, 62 | :sensor => 'DS18B20' 63 | } 64 | } 65 | 66 | json_payload = JSON[payload] 67 | logger.info "new measurement: #{json_payload}" 68 | begin 69 | MQTT::Client.connect(:host => broker_address, :username => ENV['MQTT_USER'], :password => ENV['MQTT_PASSWORD'],) do |c| 70 | c.publish('sensors', json_payload) 71 | end 72 | rescue Exception => e 73 | logger.info "unable to connect or publish to MQTT client: #{e.message}" 74 | end 75 | end 76 | 77 | scheduler.join 78 | -------------------------------------------------------------------------------- /telegraf/README.md: -------------------------------------------------------------------------------- 1 | # Telegraf 2 | 3 | This container runs [Telegraf](https://www.influxdata.com/time-series-platform/telegraf) consuming [MQTT](https://mqtt.org/) messages posted by the various sensors and pushing the data into [InfluxDB](https://www.influxdata.com/). 4 | 5 | ## Testing 6 | 7 | On your local machine you can replicate the MQTT -> Telegraf -> InfluxDB setup by running the following commands from within the _telegraf_ subdirectory: 8 | 9 | ```sh 10 | docker build . -t telegraf 11 | docker network create telegraf 12 | docker run -d --rm -p 1883:1883 -p 9001:9001 --name mqtt -v $PWD/../mqtt/mosquitto.conf:/mosquitto/config/mosquitto.conf --network telegraf eclipse-mosquitto 13 | docker run -d --rm -p 8086:8086 --name influxdb -v influxdb:/var/lib/influxdb --network telegraf influxdb:1.8 14 | docker run --network telegraf telegraf 15 | ``` 16 | 17 | Now you can now connect to the MQTT queue using [MQTT Explorer](http://mqtt-explorer.com/) and send test messages. 18 | 19 | To cleanup: 20 | 21 | ```sh 22 | docker stop mqtt 23 | docker stop influxdb 24 | docker network rm telegraf 25 | ``` 26 | 27 | ### InfluxDB 28 | 29 | In order to inspect or modify the data stored in the Influx database you can connect directly to the _influxdb_ container and start the [`influx` CLI](https://docs.influxdata.com/influxdb/v1.8/tools/shell/): 30 | 31 | ```sh 32 | $ balena ssh influxdb 33 | ? Select a device amazing-smoke (a36de3) 34 | root@265d8274d16b:/# influx 35 | Connected to http://localhost:8086 version 1.8.0 36 | InfluxDB shell version: 1.8.0 37 | ``` 38 | 39 | In order to get human-readable dates use the `precision rfc3339` command: 40 | 41 | ```sh 42 | > precision rfc3339 43 | > use weather 44 | Using database weather 45 | > show measurements 46 | name: measurements 47 | name 48 | ---- 49 | humidity 50 | rain 51 | temperature 52 | water-temperature 53 | wind-direction 54 | wind-speed 55 | ``` 56 | 57 | To select the entries of a measurement: 58 | 59 | ```sh 60 | > SELECT * FROM "water-temperature" 61 | 2021-06-18T06:05:05Z DS18B20 22.0625 sensors 62 | ... 63 | ``` 64 | 65 | To delete entries from a measurement use te [`DROP SERIES`](https://docs.influxdata.com/influxdb/v1.8/query_language/manage-database/#drop-series-from-the-index-with-drop-series) query: 66 | 67 | ```sh 68 | > DROP SERIES FROM "water-temperature" 69 | ``` 70 | 71 | ## References 72 | 73 | * Telegraf [configuration](https://github.com/influxdata/telegraf/blob/master/docs/CONFIGURATION.md) 74 | * Telegraf [troubleshooting](https://docs.influxdata.com/telegraf/v1.17/administration/troubleshooting/) 75 | * Telegraf [JSON input data format](https://docs.influxdata.com/telegraf/v1.18/data_formats/input/json/) 76 | * [plugins](https://archive.docs.influxdata.com/telegraf/v1.8/plugins) 77 | -------------------------------------------------------------------------------- /anemometer/anemometer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import datetime 5 | import json 6 | import os 7 | import schedule 8 | from signal import pause 9 | import threading 10 | import time 11 | 12 | import paho.mqtt.client as mqtt 13 | from dateutil.tz import tzutc 14 | from gpiozero import Button 15 | 16 | RECORDING_INTERVAL = os.getenv('SAMPLE_RATE', 900) 17 | print("SAMPLE_RATE set to " + str(RECORDING_INTERVAL)) 18 | 19 | 20 | def run_continuously(interval=1): 21 | """Continuously run, while executing pending jobs at each 22 | elapsed time interval. 23 | @return cease_continuous_run: threading. Event which can 24 | be set to cease continuous run. Please note that it is 25 | *intended behavior that run_continuously() does not run 26 | missed jobs*. For example, if you've registered a job that 27 | should run every minute and you set a continuous run 28 | interval of one hour then your job won't be run 60 times 29 | at each interval but only once. 30 | """ 31 | cease_continuous_run = threading.Event() 32 | 33 | class ScheduleThread(threading.Thread): 34 | @classmethod 35 | def run(cls): 36 | while not cease_continuous_run.is_set(): 37 | schedule.run_pending() 38 | time.sleep(interval) 39 | 40 | continuous_thread = ScheduleThread() 41 | continuous_thread.start() 42 | return cease_continuous_run 43 | 44 | 45 | class Anemometer: 46 | """Defines the an anemometer sensor""" 47 | 48 | BASE_SPEED = 2.4 # km/h 49 | MEASURE_INTERVAL = 60 50 | 51 | _count = 0 52 | _speed = 0 53 | 54 | def __init__(self): 55 | self.counter = Button(5) 56 | self.counter.when_pressed = self.tick 57 | self._lock = threading.Lock() 58 | 59 | schedule.every(self.MEASURE_INTERVAL).seconds.do(self.measure) 60 | 61 | def tick(self): 62 | with self._lock: 63 | self._count += 1 64 | 65 | def speed(self): 66 | return self._speed 67 | 68 | def measure(self): 69 | with self._lock: 70 | current_count = self._count 71 | self._speed = round((self.BASE_SPEED * 1000 * current_count) / (3600 * self.MEASURE_INTERVAL), 0) 72 | self._count = 0 73 | 74 | 75 | anemometer = Anemometer() 76 | 77 | broker_address = os.environ.get('MQTT_BROKER') or "mqtt" 78 | client = mqtt.Client("1") 79 | if "MQTT_USER" in os.environ and "MQTT_PASSWORD" in os.environ: 80 | client.username_pw_set(username=os.environ.get('MQTT_USER'),password=os.environ.get('MQTT_PASSWORD')) 81 | 82 | def record(): 83 | client.connect(broker_address) 84 | json_body = [ 85 | { 86 | "time": str('{:%Y-%m-%dT%H:%M:%S}'.format(datetime.datetime.now(tzutc()))), 87 | "measurement": "wind-speed", 88 | "fields": { 89 | "value": anemometer.speed(), 90 | "sensor": "anemometer" 91 | } 92 | } 93 | ] 94 | 95 | print("JSON body = " + str(json_body)) 96 | msg_info = client.publish("sensors", json.dumps(json_body)) 97 | if not msg_info.is_published(): 98 | msg_info.wait_for_publish() 99 | client.disconnect() 100 | 101 | 102 | schedule.every(RECORDING_INTERVAL).seconds.do(record) 103 | run_continuously() 104 | 105 | pause() 106 | -------------------------------------------------------------------------------- /raingauge/raingauge.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os 4 | import schedule 5 | from signal import pause 6 | import threading 7 | import time 8 | 9 | import paho.mqtt.client as mqtt 10 | from dateutil.tz import tzutc 11 | from gpiozero import Button 12 | 13 | GPIO_BUTTON = 6 14 | RECORDING_INTERVAL = os.getenv('SAMPLE_RATE', 900) 15 | print("SAMPLE_RATE set to " + str(RECORDING_INTERVAL)) 16 | 17 | 18 | def run_continuously(interval=1): 19 | """Continuously run, while executing pending jobs at each 20 | elapsed time interval. 21 | @return cease_continuous_run: threading. Event which can 22 | be set to cease continuous run. Please note that it is 23 | *intended behavior that run_continuously() does not run 24 | missed jobs*. For example, if you've registered a job that 25 | should run every minute and you set a continuous run 26 | interval of one hour then your job won't be run 60 times 27 | at each interval but only once. 28 | """ 29 | cease_continuous_run = threading.Event() 30 | 31 | class ScheduleThread(threading.Thread): 32 | @classmethod 33 | def run(cls): 34 | while not cease_continuous_run.is_set(): 35 | schedule.run_pending() 36 | time.sleep(interval) 37 | 38 | continuous_thread = ScheduleThread() 39 | continuous_thread.start() 40 | return cease_continuous_run 41 | 42 | 43 | class Raingauge: 44 | """Defines the an raingauge sensor""" 45 | 46 | BUCKET_CAPACITY = 0.2794 # mm 47 | MEASURE_INTERVAL = 180 48 | RESET_TIME = "23:59" 49 | 50 | _count = 0 51 | _level = 0 52 | 53 | def __init__(self): 54 | self.counter = Button(GPIO_BUTTON) 55 | self.counter.when_pressed = self.tick 56 | self._lock = threading.Lock() 57 | 58 | schedule.every(self.MEASURE_INTERVAL).seconds.do(self.measure) 59 | schedule.every().day.at(self.RESET_TIME).do(self.reset) 60 | 61 | def tick(self): 62 | with self._lock: 63 | self._count += 1 64 | 65 | def level(self): 66 | return self._level 67 | 68 | def measure(self): 69 | with self._lock: 70 | self._level = round(self.BUCKET_CAPACITY * self._count, 1) 71 | 72 | def reset(self): 73 | print("Resetting timer" + str('{:%Y-%m-%dT%H:%M:%S}'.format(datetime.datetime.now(tzutc())))) 74 | with self._lock: 75 | self._count = 0 76 | 77 | 78 | raingauge = Raingauge() 79 | 80 | broker_address = os.environ.get('MQTT_BROKER') or "mqtt" 81 | client = mqtt.Client("1") 82 | if "MQTT_USER" in os.environ and "MQTT_PASSWORD" in os.environ: 83 | client.username_pw_set(username=os.environ.get('MQTT_USER'),password=os.environ.get('MQTT_PASSWORD')) 84 | 85 | def record(): 86 | client.connect(broker_address) 87 | json_body = [ 88 | { 89 | "time": str('{:%Y-%m-%dT%H:%M:%S}'.format(datetime.datetime.now(tzutc()))), 90 | "measurement": "rain", 91 | "fields": { 92 | "value": raingauge.level(), 93 | "sensor": "raingauge" 94 | } 95 | } 96 | ] 97 | 98 | print("JSON body = " + str(json_body)) 99 | msg_info = client.publish("sensors", json.dumps(json_body)) 100 | if not msg_info.is_published(): 101 | msg_info.wait_for_publish() 102 | client.disconnect() 103 | 104 | 105 | schedule.every(RECORDING_INTERVAL).seconds.do(record) 106 | run_continuously() 107 | 108 | pause() 109 | -------------------------------------------------------------------------------- /humidity/sht30.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import datetime 5 | import json 6 | import os 7 | import schedule 8 | import smbus2 9 | from signal import pause 10 | import threading 11 | import time 12 | 13 | import paho.mqtt.client as mqtt 14 | from dateutil.tz import tzutc 15 | 16 | 17 | RECORDING_INTERVAL = os.getenv('SAMPLE_RATE', 900) 18 | print("SAMPLE_RATE set to " + str(RECORDING_INTERVAL)) 19 | 20 | 21 | def run_continuously(interval=1): 22 | """Continuously run, while executing pending jobs at each 23 | elapsed time interval. 24 | @return cease_continuous_run: threading. Event which can 25 | be set to cease continuous run. Please note that it is 26 | *intended behavior that run_continuously() does not run 27 | missed jobs*. For example, if you've registered a job that 28 | should run every minute and you set a continuous run 29 | interval of one hour then your job won't be run 60 times 30 | at each interval but only once. 31 | """ 32 | cease_continuous_run = threading.Event() 33 | 34 | class ScheduleThread(threading.Thread): 35 | @classmethod 36 | def run(cls): 37 | while not cease_continuous_run.is_set(): 38 | schedule.run_pending() 39 | time.sleep(interval) 40 | 41 | continuous_thread = ScheduleThread() 42 | continuous_thread.start() 43 | return cease_continuous_run 44 | 45 | 46 | class Sht30: 47 | """Defines the an SHT-30 sensor""" 48 | 49 | def __init__(self): 50 | self._bus = smbus2.SMBus(1) 51 | self._lock = threading.Lock() 52 | 53 | def temperature(self): 54 | raw = self.data() 55 | return ((((raw[0] * 256.0) + raw[1]) * 175) / 65535.0) - 45 56 | 57 | def humidity(self): 58 | raw = self.data() 59 | return 100 * (raw[3] * 256 + raw[4]) / 65535.0 60 | 61 | def data(self): 62 | with self._lock: 63 | # SHT30 address, 0x44(68) 64 | # Send measurement command, 0x2C(44) 65 | # 0x06(06) High repeatability measurement 66 | self._bus.write_i2c_block_data(0x44, 0x2C, [0x06]) 67 | 68 | time.sleep(0.5) 69 | 70 | # SHT30 address, 0x44(68) 71 | # Read data back from 0x00(00), 6 bytes 72 | # cTemp MSB, cTemp LSB, cTemp CRC, Humidity MSB, Humidity LSB, Humidity CRC 73 | raw = self._bus.read_i2c_block_data(0x44, 0x00, 6) 74 | return raw 75 | 76 | 77 | sht30 = Sht30() 78 | 79 | broker_address = os.environ.get('MQTT_BROKER') or "mqtt" 80 | client = mqtt.Client("1") 81 | if "MQTT_USER" in os.environ and "MQTT_PASSWORD" in os.environ: 82 | client.username_pw_set(username=os.environ.get('MQTT_USER'),password=os.environ.get('MQTT_PASSWORD')) 83 | 84 | def record(): 85 | client.connect(broker_address) 86 | json_body = [ 87 | { 88 | "time": str('{:%Y-%m-%dT%H:%M:%S}'.format(datetime.datetime.now(tzutc()))), 89 | "measurement": "humidity", 90 | "fields": { 91 | "humidity": sht30.humidity(), 92 | "temperature": sht30.temperature(), 93 | "sensor": "SHT-30" 94 | } 95 | } 96 | ] 97 | 98 | print("JSON body = " + str(json_body)) 99 | msg_info = client.publish("sensors", json.dumps(json_body)) 100 | if not msg_info.is_published(): 101 | msg_info.wait_for_publish() 102 | client.disconnect() 103 | 104 | 105 | schedule.every(RECORDING_INTERVAL).seconds.do(record) 106 | run_continuously() 107 | 108 | pause() 109 | -------------------------------------------------------------------------------- /api/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionpack (7.0.3) 5 | actionview (= 7.0.3) 6 | activesupport (= 7.0.3) 7 | rack (~> 2.0, >= 2.2.0) 8 | rack-test (>= 0.6.3) 9 | rails-dom-testing (~> 2.0) 10 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 11 | actionview (7.0.3) 12 | activesupport (= 7.0.3) 13 | builder (~> 3.1) 14 | erubi (~> 1.4) 15 | rails-dom-testing (~> 2.0) 16 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 17 | activesupport (7.0.3) 18 | concurrent-ruby (~> 1.0, >= 1.0.2) 19 | i18n (>= 1.6, < 2) 20 | minitest (>= 5.1) 21 | tzinfo (~> 2.0) 22 | ast (2.4.2) 23 | backport (1.2.0) 24 | benchmark (0.2.0) 25 | builder (3.2.4) 26 | concurrent-ruby (1.1.10) 27 | crass (1.0.6) 28 | diff-lcs (1.5.0) 29 | dotenv (2.7.6) 30 | e2mmap (0.1.0) 31 | erubi (1.10.0) 32 | httparty (0.20.0) 33 | mime-types (~> 3.0) 34 | multi_xml (>= 0.5.2) 35 | i18n (1.10.0) 36 | concurrent-ruby (~> 1.0) 37 | jaro_winkler (1.5.4) 38 | kramdown (2.4.0) 39 | rexml 40 | kramdown-parser-gfm (1.1.0) 41 | kramdown (~> 2.0) 42 | loofah (2.18.0) 43 | crass (~> 1.0.2) 44 | nokogiri (>= 1.5.9) 45 | method_source (1.0.0) 46 | mime-types (3.4.1) 47 | mime-types-data (~> 3.2015) 48 | mime-types-data (3.2022.0105) 49 | mini_portile2 (2.8.0) 50 | minitest (5.15.0) 51 | multi_json (1.15.0) 52 | multi_xml (0.6.0) 53 | mustermann (1.1.1) 54 | ruby2_keywords (~> 0.0.1) 55 | nokogiri (1.13.6) 56 | mini_portile2 (~> 2.8.0) 57 | racc (~> 1.4) 58 | parallel (1.22.1) 59 | parser (3.1.2.0) 60 | ast (~> 2.4.1) 61 | racc (1.6.0) 62 | rack (2.2.3.1) 63 | rack-protection (2.2.0) 64 | rack 65 | rack-test (1.1.0) 66 | rack (>= 1.0, < 3) 67 | rails-dom-testing (2.0.3) 68 | activesupport (>= 4.2.0) 69 | nokogiri (>= 1.6) 70 | rails-html-sanitizer (1.4.2) 71 | loofah (~> 2.3) 72 | railties (7.0.3) 73 | actionpack (= 7.0.3) 74 | activesupport (= 7.0.3) 75 | method_source 76 | rake (>= 12.2) 77 | thor (~> 1.0) 78 | zeitwerk (~> 2.5) 79 | rainbow (3.1.1) 80 | rake (13.0.6) 81 | regexp_parser (2.5.0) 82 | reverse_markdown (2.1.1) 83 | nokogiri 84 | rexml (3.2.5) 85 | rspec (3.11.0) 86 | rspec-core (~> 3.11.0) 87 | rspec-expectations (~> 3.11.0) 88 | rspec-mocks (~> 3.11.0) 89 | rspec-core (3.11.0) 90 | rspec-support (~> 3.11.0) 91 | rspec-expectations (3.11.0) 92 | diff-lcs (>= 1.2.0, < 2.0) 93 | rspec-support (~> 3.11.0) 94 | rspec-mocks (3.11.1) 95 | diff-lcs (>= 1.2.0, < 2.0) 96 | rspec-support (~> 3.11.0) 97 | rspec-rails (5.1.2) 98 | actionpack (>= 5.2) 99 | activesupport (>= 5.2) 100 | railties (>= 5.2) 101 | rspec-core (~> 3.10) 102 | rspec-expectations (~> 3.10) 103 | rspec-mocks (~> 3.10) 104 | rspec-support (~> 3.10) 105 | rspec-support (3.11.0) 106 | rubocop (1.30.0) 107 | parallel (~> 1.10) 108 | parser (>= 3.1.0.0) 109 | rainbow (>= 2.2.2, < 4.0) 110 | regexp_parser (>= 1.8, < 3.0) 111 | rexml (>= 3.2.5, < 4.0) 112 | rubocop-ast (>= 1.18.0, < 2.0) 113 | ruby-progressbar (~> 1.7) 114 | unicode-display_width (>= 1.4.0, < 3.0) 115 | rubocop-ast (1.18.0) 116 | parser (>= 3.1.1.0) 117 | ruby-progressbar (1.11.0) 118 | ruby2_keywords (0.0.5) 119 | sinatra (2.2.0) 120 | mustermann (~> 1.0) 121 | rack (~> 2.2) 122 | rack-protection (= 2.2.0) 123 | tilt (~> 2.0) 124 | sinatra-contrib (2.2.0) 125 | multi_json 126 | mustermann (~> 1.0) 127 | rack-protection (= 2.2.0) 128 | sinatra (= 2.2.0) 129 | tilt (~> 2.0) 130 | solargraph (0.45.0) 131 | backport (~> 1.2) 132 | benchmark 133 | bundler (>= 1.17.2) 134 | diff-lcs (~> 1.4) 135 | e2mmap 136 | jaro_winkler (~> 1.5) 137 | kramdown (~> 2.3) 138 | kramdown-parser-gfm (~> 1.1) 139 | parser (~> 3.0) 140 | reverse_markdown (>= 1.0.5, < 3) 141 | rubocop (>= 0.52) 142 | thor (~> 1.0) 143 | tilt (~> 2.0) 144 | yard (~> 0.9, >= 0.9.24) 145 | thor (1.2.1) 146 | tilt (2.0.10) 147 | tzinfo (2.0.4) 148 | concurrent-ruby (~> 1.0) 149 | unicode-display_width (2.1.0) 150 | webrick (1.7.0) 151 | yard (0.9.27) 152 | webrick (~> 1.7.0) 153 | zeitwerk (2.5.4) 154 | 155 | PLATFORMS 156 | ruby 157 | 158 | DEPENDENCIES 159 | dotenv 160 | httparty 161 | rack-test 162 | rake 163 | rspec 164 | rspec-rails 165 | rubocop 166 | sinatra 167 | sinatra-contrib 168 | solargraph 169 | tzinfo 170 | 171 | BUNDLED WITH 172 | 2.1.4 173 | -------------------------------------------------------------------------------- /windvane/windvane.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import datetime 5 | import json 6 | import os 7 | import threading 8 | import time 9 | 10 | import paho.mqtt.client as mqtt 11 | import pingouin as pg 12 | import schedule 13 | from dateutil.tz import * 14 | from gpiozero import MCP3008 15 | 16 | 17 | RECORDING_INTERVAL = os.getenv('SAMPLE_RATE', 900) 18 | print("SAMPLE_RATE set to " + str(RECORDING_INTERVAL)) 19 | 20 | 21 | def run_continuously(interval=1): 22 | """Continuously run, while executing pending jobs at each 23 | elapsed time interval. 24 | @return cease_continuous_run: threading. Event which can 25 | be set to cease continuous run. Please note that it is 26 | *intended behavior that run_continuously() does not run 27 | missed jobs*. For example, if you've registered a job that 28 | should run every minute and you set a continuous run 29 | interval of one hour then your job won't be run 60 times 30 | at each interval but only once. 31 | """ 32 | cease_continuous_run = threading.Event() 33 | 34 | class ScheduleThread(threading.Thread): 35 | @classmethod 36 | def run(cls): 37 | while not cease_continuous_run.is_set(): 38 | schedule.run_pending() 39 | time.sleep(interval) 40 | 41 | continuous_thread = ScheduleThread() 42 | continuous_thread.start() 43 | return cease_continuous_run 44 | 45 | 46 | class Windvane: 47 | """Defines the an windvane""" 48 | 49 | MEASURE_INTERVAL = 1 50 | RECORD_INTERVAL = 10 51 | 52 | _data = [] 53 | _wind_direction = 0 54 | 55 | def __init__(self): 56 | self._adc = MCP3008(channel=0) 57 | self._lock = threading.Lock() 58 | 59 | schedule.every(self.MEASURE_INTERVAL).seconds.do(self.measure) 60 | schedule.every(self.RECORD_INTERVAL).seconds.do(self.record) 61 | 62 | def current_direction(self): 63 | return self._wind_direction 64 | 65 | def measure(self): 66 | with self._lock: 67 | # print("voltage=%0.4f" % (self._adc.value * 3.3)) 68 | windval = round(self._adc.value * 3.3, 2) 69 | if 0.3 <= windval <= 0.5: 70 | wind = 0.0 71 | self._data.append(wind) 72 | 73 | if 1.3 <= windval <= 1.5: 74 | wind = 22.5 75 | self._data.append(wind) 76 | 77 | if 1.1 <= windval <= 1.3: 78 | wind = 45.0 79 | self._data.append(wind) 80 | 81 | if 2.76 <= windval <= 2.78: 82 | wind = 67.5 83 | self._data.append(wind) 84 | 85 | if 2.71 <= windval <= 2.73: 86 | wind = 90.0 87 | self._data.append(wind) 88 | 89 | if 2.8 <= windval <= 2.9: 90 | wind = 112.5 91 | self._data.append(wind) 92 | 93 | if 2.1 <= windval <= 2.3: 94 | wind = 135.0 95 | self._data.append(wind) 96 | 97 | if 2.4 <= windval <= 2.6: 98 | wind = 157.5 99 | self._data.append(wind) 100 | 101 | if 1.7 <= windval <= 1.9: 102 | wind = 180.0 103 | self._data.append(wind) 104 | 105 | if 1.9 <= windval <= 2.1: 106 | wind = 202.5 107 | self._data.append(wind) 108 | 109 | if 0.6 <= windval <= 0.8: 110 | wind = 225.0 111 | self._data.append(wind) 112 | 113 | if 0.7 <= windval <= 0.9: 114 | wind = 247.5 115 | self._data.append(wind) 116 | 117 | if 0.0 <= windval <= 0.2: 118 | wind = 270.0 119 | self._data.append(wind) 120 | 121 | if 0.30 <= windval <= 0.35: 122 | wind = 292.5 123 | self._data.append(wind) 124 | 125 | if 0.2 <= windval <= 0.25: 126 | wind = 315.0 127 | self._data.append(wind) 128 | 129 | if 0.5 <= windval <= 0.7: 130 | wind = 337.5 131 | self._data.append(wind) 132 | 133 | def record(self): 134 | with self._lock: 135 | radian_angles = pg.convert_angles(self._data, low=0, high=360, positive=True) 136 | radian_mean = pg.circ_mean(radian_angles) 137 | if radian_mean < 0: 138 | radian_mean = radian_mean + (2 * 3.14) 139 | # print("mean=%0.4f" % radian_mean) 140 | self._wind_direction = round((radian_mean * 57.29578), 4) 141 | self._data = [] 142 | 143 | 144 | windvane = Windvane() 145 | broker_address = os.environ.get('MQTT_BROKER') or "mqtt" 146 | client = mqtt.Client("1") 147 | if "MQTT_USER" in os.environ and "MQTT_PASSWORD" in os.environ: 148 | client.username_pw_set(username=os.environ.get('MQTT_USER'),password=os.environ.get('MQTT_PASSWORD')) 149 | 150 | def record(): 151 | client.connect(broker_address) 152 | json_body = [ 153 | { 154 | "time": str('{:%Y-%m-%dT%H:%M:%S}'.format(datetime.datetime.now(tzutc()))), 155 | "measurement": "wind-direction", 156 | "fields": { 157 | "value": int(windvane.current_direction()), 158 | "sensor": "windvane" 159 | } 160 | } 161 | ] 162 | 163 | print("JSON body = " + str(json_body)) 164 | msg_info = client.publish("sensors", json.dumps(json_body)) 165 | if not msg_info.is_published(): 166 | msg_info.wait_for_publish() 167 | client.disconnect() 168 | 169 | 170 | schedule.every(RECORDING_INTERVAL).seconds.do(record) 171 | run_continuously() 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Balena Weather Station 2 | 3 | A Raspberry Pi based weather station, running a [extensible](#extending-the-balena-weather-station) Balena [multi-container application](https://www.balena.io/docs/learn/develop/multicontainer/) inspired by the [Raspberry Pi weather station](https://projects.raspberrypi.org/en/projects/build-your-own-weather-station/0) project. 4 | 5 | ![Weather Station](./images/weather_station.png) 6 | 7 | The following sections describe the hardware, wiring and configuration of the Balena Weather Station. 8 | 9 | 10 | 11 | - [Balena Weather Station](#balena-weather-station) 12 | - [Hardware](#hardware) 13 | - [Wiring](#wiring) 14 | - [Deploy the code](#deploy-the-code) 15 | - [Via Deploy with Balena](#via-deploy-with-balena) 16 | - [Via the `balena` CLI](#via-the-balena-cli) 17 | - [Configure the Balena Weather Station](#configure-the-balena-weather-station) 18 | - [DT parameters and overlays](#dt-parameters-and-overlays) 19 | - [Device Variables](#device-variables) 20 | - [Access to the Grafana interface](#access-to-the-grafana-interface) 21 | - [Individual service descriptions](#individual-service-descriptions) 22 | - [Sensors](#sensors) 23 | - [MQTT, Telegraf and InfluxDB](#mqtt-telegraf-and-influxdb) 24 | - [UI and API](#ui-and-api) 25 | - [Powering the Raspberry Pi via the 5V rail](#powering-the-raspberry-pi-via-the-5v-rail) 26 | - [Extending the Balena Weather Station](#extending-the-balena-weather-station) 27 | - [Contributing](#contributing) 28 | - [Troubleshooting](#troubleshooting) 29 | 30 | 31 | 32 | ## Hardware 33 | 34 | ![Raspberry Pi 3](./images/raspberry_pi.png) 35 | 36 | Let's start with the hardware used for this project. 37 | 38 | - 1 [Raspberry Pi 3](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) - the heart of the weather station. 39 | Balena Weather Station is also compatible with the Raspberry Pi 4. 40 | - 1 [Sparkfun Weather Meter Kit](https://www.sparkfun.com/products/15901) - the main weather station components, including an anemometer, wind vane and rain gauge. 41 | - 1 [Prototyping HAT for Raspberry Pi](https://www.robotshop.com/en/prototyping-hat-raspberry-pi-b-2ba3b.html) - I aimed to build a permanent weather station (see picture above). For this reason, I opted for the Prototyping HAT and soldering. 42 | You can also use a [GPIO extender board](https://www.sparkfun.com/products/13717) with a breadboard for a less permanent solution. 43 | - 1 [MCP3008](https://www.microchip.com/en-us/product/MCP3008) - a 8-channel, 10-bit ADC with SPI interface. 44 | It is used to convert the analog voltage provided by the wind vane into a digital value. 45 | - 1 [SHT-30](https://www.adafruit.com/product/4099) - a wheater proof humidity sensor that includes a temperature sensor. 46 | The temperature measurements by the SHT-30 are also stored in InfluxDB. 47 | However, the default Grafana dashboard does not include them. 48 | - 1 [DS18B20](https://www.amazon.com/Eiechip-Waterproof-Temperature-Thermometer-Resistance/dp/B07MB1J43W/) - standard 1-wire bus waterproof temperature sensor. 49 | - 2 4.7kΩ resistors - used for the wind vane's voltage divider circuit as well as a pull-up resistor for the temperature sensor. 50 | - 1 [Raspberry Pi IP54 Outdoor Project Enclosure](https://sixfab.com/product/raspberry-pi-ip54-outdoor-iot-project-enclosure/) - a weatherproof enclosure for the Raspberry Pi. 51 | 52 | ### Wiring 53 | 54 | The following diagram provides the schematics of the Balena Weather Station. 55 | The anemometer, wind vane and rain gauge are symbolised by their main electric component. 56 | 57 | **NOTE**: The Sparkfun Weather station comes per default with 6-pin RJ11 connectors. 58 | The middle four pins are connected, but only two cables are used. 59 | Refer to the [Sparkfun Weather Meter Kit manual](https://cdn.sparkfun.com/assets/d/1/e/0/6/DS-15901-Weather_Meter.pdf) to see which cables are relevant for each of the components. 60 | 61 | ![Balena Weather Wiring](./images/balena_weather_bb.png) 62 | 63 | ## Deploy the code 64 | 65 | ### Via [Deploy with Balena](https://www.balena.io/docs/learn/deploy/deploy-with-balena-button/) 66 | 67 | Running this project is as simple as deploying it to a balenaCloud application. 68 | You can do it in just one click by using the button below: 69 | 70 | [![balena deploy button](https://www.balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/hferentschik/balena-weather) 71 | 72 | Follow instructions, click Add a Device and flash an SD card with that OS image downloaded from balenaCloud. 73 | Enjoy the magic 🌟Over-The-Air🌟! 74 | 75 | ### Via the `balena` [CLI](https://www.balena.io/docs/reference/balena-cli/) 76 | 77 | If you would like to add more services, get the `balena` CLI and follow these steps: 78 | 79 | - Sign up on [balena.io](https://dashboard.balena.io/signup) 80 | - Create a new application on balenaCloud. 81 | - Clone this repository to your local workspace. 82 | - Using the `balena` [CLI](https://www.balena.io/docs/reference/cli/), push the code with `balena push ` 83 | - See the magic happening; your device is getting updated 🌟Over-The-Air🌟! 84 | 85 | ## Configure the Balena Weather Station 86 | 87 | On the software side, the Balena Weather Station is built as a [multi container application](https://www.balena.io/docs/learn/develop/multicontainer/). 88 | Services are defined in [docker-compose.yml](./docker-compose.yml). 89 | 90 | ![balena-weather-station-balenaCloud](https://user-images.githubusercontent.com/173156/162697086-bbefacde-8c5d-45c3-9083-caf1bb614b72.png) 91 | 92 | ### DT parameters and overlays 93 | 94 | For the sensors to work, the Balena [device or fleet configuration](https://github.com/balena-io/balena-fleet-management-masterclass#3-configuration) needs to enable the _w1-gpio_ overlay as well as set the DT parameters _"i2c_arm=on","spi=on"_. 95 | 96 | ![Balena Device Configuration](./images/device_configuration.png) 97 | 98 | ### Device Variables 99 | 100 | The following Device Variables are supported by the Balena Weather Station: 101 | 102 | ![Balena Device Variables](./images/device_variables.png) 103 | 104 | Variable Name | Value | Description | Default 105 | ------------ | ------------- | ------------- | ------------- 106 | **`SAMPLE_RATE`** | `INT` | The default sample rate for each container is 15 minutes (specified in seconds). | 900 107 | **`MQTT_USER`** | `STRING` | Username to authenticate with the MQTT message broker | 108 | **`MQTT_PASSWORD`** | `STRING` | Password to authenticate with the MQTT message broker | 109 | **`LATITUDE`** | `FLOAT` | Specify your `LATITUDE` for the `api` service that calculates sunrise and sunset | 110 | **`LONGITUDE`** | `FLOAT` | Specify your `LONGITUDE` for the `api` service that calculates sunrise and sunset | 111 | **`TIMEZONE`** | `STRING` | Defines your timezone to calculate the time of your sunrise and sunset | 112 | 113 | ### Access to the Grafana interface 114 | 115 | Once all the services are successfully deployed, you will be able to access the Weather Station Grafana interface using the local device address `http:///weather` or the Balena public address `http:///weather` respectively. 116 | 117 | **TIP**: Assuming your fleet is called _weather_, you can retrieve the public URL using: 118 | 119 | ```sh 120 | balena device public-url $(balena devices -a weather --json | jq -r .[].id) 121 | ``` 122 | 123 | ![Grafana Dash](./images/dash.png) 124 | 125 | ## Individual service descriptions 126 | 127 | The following sections describe the various services in more detail. 128 | 129 | ### Sensors 130 | 131 | Each service is contained in its own subdirectory. 132 | The README in each subdirectory provides additional information for each service. 133 | 134 | - [Anemometer](./anemometer/README.md) - Anemometer (wind speed) sensor of the weather station. 135 | - [Humidity](./humidity/README.md) - Humidity and temperature sensor SHT-30. 136 | - [Raingauge](./raingauge/README.md) - Raingauge sensor of the weather station. 137 | - [Temperature](/temperature/README.md) - Additional DS18B20 temperature sensor. 138 | I am using an additonal temperetature sensor to the SHT-30 temperature sensor which I place in the shade. 139 | The SHT-30 on the other hand is in direct sunlight. 140 | - [Windvane](/windvane/README.md) - Windvane sensor of the weather station. 141 | 142 | ### MQTT, Telegraf and InfluxDB 143 | 144 | - [MQTT](./mqtt/README.md) - [Eclipse Mosquitto](https://hub.docker.com/r/arm64v8/eclipse-mosquitto) container which acts as message broker to which all sensors are sending their data. 145 | The Telegraf container reads from the Mosquitto queue and pushes the metrics into InfluxDB. 146 | - [Telegraf](./telegraf/README.md) - Part of the [TIG](https://hackmd.io/@lnu-iot/tig-stack) stack to consume and display sensor data. 147 | - InfluxDB - Time series database storing the sensor data. 148 | This is the storage component of the [TIG](https://hackmd.io/@lnu-iot/tig-stack) stack. 149 | It uses a default [InfluxDB DockerHub image](https://hub.docker.com/_/influxdb). 150 | 151 | ### UI and API 152 | 153 | - [NGINX](./nginx) - NGINX listening on port 80 and acting as reverse proxy. 154 | - [API](./api/README.md) - A Ruby based [Sinatra](http://sinatrarb.com) app used for exposing REST APIs for Grafana (using the JSON datasource plugin). 155 | - [Grafana Dashboard](./dashboard/README.md) - the Grafana dashboard displaying all data. 156 | 157 | ## Powering the Raspberry Pi via the 5V rail 158 | 159 | I decided to power the Raspberry Pi via the 5V power rail. 160 | The following links provide information on how to do so. 161 | 162 | - [Raspberry Pi Fuse](https://www.petervis.com/Raspberry_PI/Raspberry_Pi_Dead/Raspberry_Pi_Fuse.html) 163 | - [Power requirements of the Pi](https://raspberrypi.stackexchange.com/questions/51615/raspberry-pi-power-limitations) 164 | 165 | ## Extending the Balena Weather Station 166 | 167 | The [balena-weather](https://github.com/hferentschik/balena-weather) GitHub repo contains a [_solar_](https://github.com/hferentschik/balena-weather/tree/solar) branch with the required changes to add electricity production data for a SolarEdge solar panel installatio. 168 | 169 | ![SolarEdge](./images/solar_edge.png) 170 | 171 | The data itself is provided by the [balena-solar-edge](https://github.com/hferentschik/balena-solar-edge) service. 172 | _balena-solar-edge_ uses the [SolarEdge Monitoring Server API](https://www.solaredge.com/sites/default/files/se_monitoring_api.pdf) to retrieve the data and then pushes the data to MQTT. 173 | 174 | The service can easily be added to [docker-compose.yml](./docker-compose.yml) like so: 175 | 176 | ```yaml 177 | solar: 178 | image: hferentschik/solar-edge:0.0.1 179 | restart: always 180 | depends_on: 181 | - mqtt 182 | ``` 183 | 184 | To keep the SolarEdge data seperate from the weather data, it gets stored into its own InfluxDB database. 185 | To do this a new Telegraf configuration file (_solar-edge.conf_) gets added to the _/etc/telegraf/telegraf.d_ directory of the telegraf service. 186 | The configuration follows the [Telegraf Best Practices](https://www.influxdata.com/blog/telegraf-best-practices/) in order to keep the configuration for the weather and solar part seperate. 187 | In particular the use of _tagpass_ is important to ensure that only data from the _solar_ MQTT topic ges added to the _solar_ database. 188 | 189 | ```toml 190 | ############################################################################### 191 | # OUTPUT PLUGINS # 192 | ############################################################################### 193 | [[outputs.influxdb]] 194 | alias = "influxdb_solar" 195 | timeout = "1s" 196 | database = "solar" 197 | urls = [ "http://influxdb:8086" ] 198 | [outputs.influxdb.tagpass] 199 | topic = [ "solar" ] 200 | 201 | ############################################################################### 202 | # INPUT PLUGINS # 203 | ############################################################################### 204 | [[inputs.mqtt_consumer]] 205 | alias = "mqtt_solar" 206 | servers = [ "mqtt:1883" ] 207 | topics = [ "solar" ] 208 | json_name_key = "measurement" 209 | data_format = "json" 210 | json_time_key = "time" 211 | json_time_format = "2006-01-02T15:04:05" 212 | tag_keys = [ 213 | "tags_name", "tags_image", "tags_model" 214 | ] 215 | # username = "mqtt" 216 | # password = "pass" 217 | ``` 218 | 219 | Last but not least, the Grafana dash needs to be updated to display the solar data. 220 | The required changes are part of _dashboard.json_ in the dashboar service. 221 | 222 | Using this approach other services can be added as well. 223 | 224 | ## Contributing 225 | 226 | Contributions, questions, and comments are all welcomed and encouraged! 227 | 228 | If you want to contribute, follow the [contribution guidelines](./CONTRIBUTING.md) when you open issues or submit pull requests. 229 | 230 | ## Troubleshooting 231 | 232 | If you have any issues feel free to add a Github issue [here](https://github.com/hferentschik/balena-weather/issues) or add questions on the [balena forums](https://forums.balena.io). 233 | -------------------------------------------------------------------------------- /dashboard/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "description": "Access current weather data", 16 | "editable": true, 17 | "gnetId": 9710, 18 | "graphTooltip": 2, 19 | "id": 1, 20 | "links": [], 21 | "panels": [ 22 | { 23 | "collapsed": false, 24 | "datasource": null, 25 | "gridPos": { 26 | "h": 1, 27 | "w": 24, 28 | "x": 0, 29 | "y": 0 30 | }, 31 | "id": 16, 32 | "panels": [], 33 | "repeat": "city", 34 | "title": "Current", 35 | "type": "row" 36 | }, 37 | { 38 | "cacheTimeout": null, 39 | "colorBackground": false, 40 | "colorValue": true, 41 | "colors": [ 42 | "#447ebc", 43 | "#629e51", 44 | "#e24d42" 45 | ], 46 | "datasource": null, 47 | "decimals": 0, 48 | "fieldConfig": { 49 | "defaults": { 50 | "custom": {} 51 | }, 52 | "overrides": [] 53 | }, 54 | "format": "celsius", 55 | "gauge": { 56 | "maxValue": 40, 57 | "minValue": -20, 58 | "show": true, 59 | "thresholdLabels": false, 60 | "thresholdMarkers": true 61 | }, 62 | "gridPos": { 63 | "h": 6, 64 | "w": 4, 65 | "x": 0, 66 | "y": 1 67 | }, 68 | "id": 2, 69 | "interval": null, 70 | "links": [], 71 | "mappingType": 1, 72 | "mappingTypes": [ 73 | { 74 | "name": "value to text", 75 | "value": 1 76 | }, 77 | { 78 | "name": "range to text", 79 | "value": 2 80 | } 81 | ], 82 | "maxDataPoints": 100, 83 | "nullPointMode": "connected", 84 | "nullText": null, 85 | "postfix": "", 86 | "postfixFontSize": "50%", 87 | "prefix": "", 88 | "prefixFontSize": "50%", 89 | "rangeMaps": [ 90 | { 91 | "from": "null", 92 | "text": "N/A", 93 | "to": "null" 94 | } 95 | ], 96 | "sparkline": { 97 | "fillColor": "rgba(31, 118, 189, 0.18)", 98 | "full": false, 99 | "lineColor": "rgb(31, 120, 193)", 100 | "show": false 101 | }, 102 | "tableColumn": "", 103 | "targets": [ 104 | { 105 | "groupBy": [ 106 | { 107 | "params": [ 108 | "$__interval" 109 | ], 110 | "type": "time" 111 | }, 112 | { 113 | "params": [ 114 | "null" 115 | ], 116 | "type": "fill" 117 | } 118 | ], 119 | "measurement": "humidity", 120 | "orderByTime": "ASC", 121 | "policy": "default", 122 | "refId": "B", 123 | "resultFormat": "time_series", 124 | "select": [ 125 | [ 126 | { 127 | "params": [ 128 | "fields_temperature" 129 | ], 130 | "type": "field" 131 | }, 132 | { 133 | "params": [], 134 | "type": "last" 135 | } 136 | ] 137 | ], 138 | "tags": [] 139 | } 140 | ], 141 | "thresholds": "0, 30", 142 | "title": "Temperature", 143 | "type": "singlestat", 144 | "valueFontSize": "80%", 145 | "valueMaps": [ 146 | { 147 | "op": "=", 148 | "text": "N/A", 149 | "value": "null" 150 | } 151 | ], 152 | "valueName": "current" 153 | }, 154 | { 155 | "cacheTimeout": null, 156 | "colorBackground": false, 157 | "colorValue": true, 158 | "colors": [ 159 | "#629e51", 160 | "#ef843c", 161 | "#e24d42" 162 | ], 163 | "datasource": null, 164 | "decimals": 1, 165 | "fieldConfig": { 166 | "defaults": { 167 | "custom": {} 168 | }, 169 | "overrides": [] 170 | }, 171 | "format": "velocityms", 172 | "gauge": { 173 | "maxValue": 25, 174 | "minValue": 0, 175 | "show": true, 176 | "thresholdLabels": false, 177 | "thresholdMarkers": true 178 | }, 179 | "gridPos": { 180 | "h": 6, 181 | "w": 4, 182 | "x": 4, 183 | "y": 1 184 | }, 185 | "id": 3, 186 | "interval": null, 187 | "links": [], 188 | "mappingType": 1, 189 | "mappingTypes": [ 190 | { 191 | "name": "value to text", 192 | "value": 1 193 | }, 194 | { 195 | "name": "range to text", 196 | "value": 2 197 | } 198 | ], 199 | "maxDataPoints": 100, 200 | "nullPointMode": "connected", 201 | "nullText": null, 202 | "postfix": "", 203 | "postfixFontSize": "50%", 204 | "prefix": "", 205 | "prefixFontSize": "50%", 206 | "rangeMaps": [ 207 | { 208 | "from": "null", 209 | "text": "N/A", 210 | "to": "null" 211 | } 212 | ], 213 | "sparkline": { 214 | "fillColor": "rgba(31, 118, 189, 0.18)", 215 | "full": false, 216 | "lineColor": "rgb(31, 120, 193)", 217 | "show": false 218 | }, 219 | "tableColumn": "", 220 | "targets": [ 221 | { 222 | "groupBy": [ 223 | { 224 | "params": [ 225 | "$__interval" 226 | ], 227 | "type": "time" 228 | }, 229 | { 230 | "params": [ 231 | "null" 232 | ], 233 | "type": "fill" 234 | } 235 | ], 236 | "measurement": "wind-speed", 237 | "orderByTime": "ASC", 238 | "policy": "default", 239 | "refId": "B", 240 | "resultFormat": "time_series", 241 | "select": [ 242 | [ 243 | { 244 | "params": [ 245 | "fields_value" 246 | ], 247 | "type": "field" 248 | }, 249 | { 250 | "params": [], 251 | "type": "last" 252 | } 253 | ] 254 | ], 255 | "tags": [] 256 | } 257 | ], 258 | "thresholds": "6,14", 259 | "title": "Wind speed", 260 | "type": "singlestat", 261 | "valueFontSize": "80%", 262 | "valueMaps": [ 263 | { 264 | "op": "=", 265 | "text": "N/A", 266 | "value": "null" 267 | } 268 | ], 269 | "valueName": "current" 270 | }, 271 | { 272 | "cacheTimeout": null, 273 | "datasource": "API", 274 | "fieldConfig": { 275 | "defaults": { 276 | "custom": {}, 277 | "mappings": [ 278 | { 279 | "id": 0, 280 | "op": "=", 281 | "text": "N/A", 282 | "type": 1, 283 | "value": "null" 284 | } 285 | ], 286 | "nullValueMode": "connected", 287 | "thresholds": { 288 | "mode": "absolute", 289 | "steps": [ 290 | { 291 | "color": "green", 292 | "value": null 293 | }, 294 | { 295 | "color": "red", 296 | "value": 80 297 | } 298 | ] 299 | }, 300 | "unit": "dateTimeAsIso" 301 | }, 302 | "overrides": [] 303 | }, 304 | "gridPos": { 305 | "h": 3, 306 | "w": 4, 307 | "x": 8, 308 | "y": 1 309 | }, 310 | "id": 13, 311 | "interval": null, 312 | "links": [], 313 | "maxDataPoints": 100, 314 | "options": { 315 | "colorMode": "value", 316 | "graphMode": "none", 317 | "justifyMode": "auto", 318 | "orientation": "horizontal", 319 | "reduceOptions": { 320 | "calcs": [ 321 | "lastNotNull" 322 | ], 323 | "fields": "/^sunrise$/", 324 | "values": false 325 | }, 326 | "textMode": "auto" 327 | }, 328 | "pluginVersion": "7.3.10", 329 | "targets": [ 330 | { 331 | "cacheDurationSeconds": 1800, 332 | "fields": [ 333 | { 334 | "jsonPath": "$.results.sunrise", 335 | "type": "string" 336 | } 337 | ], 338 | "method": "GET", 339 | "queryParams": "", 340 | "refId": "A", 341 | "urlPath": "/api/v1/sunrise" 342 | } 343 | ], 344 | "title": "Sunrise time", 345 | "type": "stat" 346 | }, 347 | { 348 | "cacheTimeout": null, 349 | "colorBackground": false, 350 | "colorValue": false, 351 | "colors": [ 352 | "#ef843c", 353 | "#629e51", 354 | "#ef843c" 355 | ], 356 | "datasource": null, 357 | "decimals": 0, 358 | "fieldConfig": { 359 | "defaults": { 360 | "custom": {} 361 | }, 362 | "overrides": [] 363 | }, 364 | "format": "percent", 365 | "gauge": { 366 | "maxValue": 100, 367 | "minValue": 0, 368 | "show": false, 369 | "thresholdLabels": false, 370 | "thresholdMarkers": true 371 | }, 372 | "gridPos": { 373 | "h": 3, 374 | "w": 4, 375 | "x": 12, 376 | "y": 1 377 | }, 378 | "id": 6, 379 | "interval": null, 380 | "links": [], 381 | "mappingType": 1, 382 | "mappingTypes": [ 383 | { 384 | "name": "value to text", 385 | "value": 1 386 | }, 387 | { 388 | "name": "range to text", 389 | "value": 2 390 | } 391 | ], 392 | "maxDataPoints": 100, 393 | "nullPointMode": "connected", 394 | "nullText": null, 395 | "postfix": "", 396 | "postfixFontSize": "50%", 397 | "prefix": "", 398 | "prefixFontSize": "50%", 399 | "rangeMaps": [ 400 | { 401 | "from": "null", 402 | "text": "N/A", 403 | "to": "null" 404 | } 405 | ], 406 | "sparkline": { 407 | "fillColor": "rgba(31, 118, 189, 0.18)", 408 | "full": false, 409 | "lineColor": "rgb(31, 120, 193)", 410 | "show": true 411 | }, 412 | "tableColumn": "", 413 | "targets": [ 414 | { 415 | "groupBy": [ 416 | { 417 | "params": [ 418 | "$__interval" 419 | ], 420 | "type": "time" 421 | }, 422 | { 423 | "params": [ 424 | "null" 425 | ], 426 | "type": "fill" 427 | } 428 | ], 429 | "measurement": "humidity", 430 | "orderByTime": "ASC", 431 | "policy": "default", 432 | "refId": "B", 433 | "resultFormat": "time_series", 434 | "select": [ 435 | [ 436 | { 437 | "params": [ 438 | "fields_humidity" 439 | ], 440 | "type": "field" 441 | }, 442 | { 443 | "params": [], 444 | "type": "last" 445 | } 446 | ] 447 | ], 448 | "tags": [] 449 | } 450 | ], 451 | "thresholds": "", 452 | "title": "Humidity", 453 | "type": "singlestat", 454 | "valueFontSize": "80%", 455 | "valueMaps": [ 456 | { 457 | "op": "=", 458 | "text": "N/A", 459 | "value": "null" 460 | } 461 | ], 462 | "valueName": "current" 463 | }, 464 | { 465 | "cacheTimeout": null, 466 | "colorBackground": false, 467 | "colorValue": false, 468 | "colors": [ 469 | "#299c46", 470 | "rgba(237, 129, 40, 0.89)", 471 | "#d44a3a" 472 | ], 473 | "datasource": null, 474 | "decimals": null, 475 | "fieldConfig": { 476 | "defaults": { 477 | "custom": {} 478 | }, 479 | "overrides": [] 480 | }, 481 | "format": "none", 482 | "gauge": { 483 | "maxValue": 100, 484 | "minValue": 0, 485 | "show": false, 486 | "thresholdLabels": false, 487 | "thresholdMarkers": true 488 | }, 489 | "gridPos": { 490 | "h": 3, 491 | "w": 4, 492 | "x": 16, 493 | "y": 1 494 | }, 495 | "id": 11, 496 | "interval": null, 497 | "links": [], 498 | "mappingType": 2, 499 | "mappingTypes": [ 500 | { 501 | "name": "value to text", 502 | "value": 1 503 | }, 504 | { 505 | "name": "range to text", 506 | "value": 2 507 | } 508 | ], 509 | "maxDataPoints": 100, 510 | "nullPointMode": "connected", 511 | "nullText": null, 512 | "postfix": "", 513 | "postfixFontSize": "50%", 514 | "prefix": "", 515 | "prefixFontSize": "50%", 516 | "rangeMaps": [ 517 | { 518 | "from": "0", 519 | "text": "↓", 520 | "to": "22.5" 521 | }, 522 | { 523 | "from": "22.5", 524 | "text": "↙", 525 | "to": "77.5" 526 | }, 527 | { 528 | "from": "77.5", 529 | "text": "←", 530 | "to": "112.5" 531 | }, 532 | { 533 | "from": "112.5", 534 | "text": "↖", 535 | "to": "157.5" 536 | }, 537 | { 538 | "from": "157.5", 539 | "text": "↑", 540 | "to": "202.5" 541 | }, 542 | { 543 | "from": "202.5", 544 | "text": "↗", 545 | "to": "247.5" 546 | }, 547 | { 548 | "from": "247.5", 549 | "text": "→", 550 | "to": "292.5" 551 | }, 552 | { 553 | "from": "292.5", 554 | "text": "↘", 555 | "to": "337.5" 556 | }, 557 | { 558 | "from": "337.5", 559 | "text": "↓", 560 | "to": "360" 561 | } 562 | ], 563 | "sparkline": { 564 | "fillColor": "rgba(31, 118, 189, 0.18)", 565 | "full": false, 566 | "lineColor": "rgb(31, 120, 193)", 567 | "show": false 568 | }, 569 | "tableColumn": "", 570 | "targets": [ 571 | { 572 | "groupBy": [ 573 | { 574 | "params": [ 575 | "$__interval" 576 | ], 577 | "type": "time" 578 | }, 579 | { 580 | "params": [ 581 | "null" 582 | ], 583 | "type": "fill" 584 | } 585 | ], 586 | "measurement": "wind-direction", 587 | "orderByTime": "ASC", 588 | "policy": "default", 589 | "refId": "B", 590 | "resultFormat": "time_series", 591 | "select": [ 592 | [ 593 | { 594 | "params": [ 595 | "fields_value" 596 | ], 597 | "type": "field" 598 | }, 599 | { 600 | "params": [], 601 | "type": "mean" 602 | } 603 | ] 604 | ], 605 | "tags": [] 606 | } 607 | ], 608 | "thresholds": "", 609 | "title": "Wind direction", 610 | "type": "singlestat", 611 | "valueFontSize": "80%", 612 | "valueMaps": [ 613 | { 614 | "op": "=", 615 | "text": "N/A", 616 | "value": "null" 617 | } 618 | ], 619 | "valueName": "current" 620 | }, 621 | { 622 | "cacheTimeout": null, 623 | "datasource": "API", 624 | "fieldConfig": { 625 | "defaults": { 626 | "custom": {}, 627 | "mappings": [ 628 | { 629 | "id": 0, 630 | "op": "=", 631 | "text": "N/A", 632 | "type": 1, 633 | "value": "null" 634 | } 635 | ], 636 | "nullValueMode": "connected", 637 | "thresholds": { 638 | "mode": "absolute", 639 | "steps": [ 640 | { 641 | "color": "green", 642 | "value": null 643 | }, 644 | { 645 | "color": "red", 646 | "value": 80 647 | } 648 | ] 649 | }, 650 | "unit": "dateTimeAsIso" 651 | }, 652 | "overrides": [] 653 | }, 654 | "gridPos": { 655 | "h": 3, 656 | "w": 4, 657 | "x": 8, 658 | "y": 4 659 | }, 660 | "id": 14, 661 | "interval": null, 662 | "links": [], 663 | "maxDataPoints": 100, 664 | "options": { 665 | "colorMode": "value", 666 | "graphMode": "none", 667 | "justifyMode": "auto", 668 | "orientation": "horizontal", 669 | "reduceOptions": { 670 | "calcs": [ 671 | "lastNotNull" 672 | ], 673 | "fields": "/^sunset$/", 674 | "values": false 675 | }, 676 | "textMode": "auto" 677 | }, 678 | "pluginVersion": "7.3.10", 679 | "targets": [ 680 | { 681 | "cacheDurationSeconds": 1800, 682 | "fields": [ 683 | { 684 | "jsonPath": "$.results.sunset", 685 | "type": "string" 686 | } 687 | ], 688 | "method": "GET", 689 | "queryParams": "", 690 | "refId": "A", 691 | "urlPath": "/api/v1/sunset" 692 | } 693 | ], 694 | "title": "Sunset time", 695 | "type": "stat" 696 | }, 697 | { 698 | "cacheTimeout": null, 699 | "datasource": null, 700 | "description": "Rain in mm per day", 701 | "fieldConfig": { 702 | "defaults": { 703 | "custom": {}, 704 | "decimals": 0, 705 | "mappings": [ 706 | { 707 | "id": 0, 708 | "op": "=", 709 | "text": "N/A", 710 | "type": 1, 711 | "value": "null" 712 | } 713 | ], 714 | "nullValueMode": "connected", 715 | "thresholds": { 716 | "mode": "absolute", 717 | "steps": [ 718 | { 719 | "color": "green", 720 | "value": null 721 | }, 722 | { 723 | "color": "red", 724 | "value": 80 725 | } 726 | ] 727 | }, 728 | "unit": "lengthmm" 729 | }, 730 | "overrides": [] 731 | }, 732 | "gridPos": { 733 | "h": 3, 734 | "w": 4, 735 | "x": 12, 736 | "y": 4 737 | }, 738 | "id": 7, 739 | "interval": null, 740 | "links": [], 741 | "maxDataPoints": 100, 742 | "options": { 743 | "colorMode": "value", 744 | "graphMode": "area", 745 | "justifyMode": "auto", 746 | "orientation": "horizontal", 747 | "reduceOptions": { 748 | "calcs": [ 749 | "lastNotNull" 750 | ], 751 | "fields": "", 752 | "values": false 753 | }, 754 | "textMode": "auto" 755 | }, 756 | "pluginVersion": "7.3.10", 757 | "targets": [ 758 | { 759 | "groupBy": [ 760 | { 761 | "params": [ 762 | "$__interval" 763 | ], 764 | "type": "time" 765 | }, 766 | { 767 | "params": [ 768 | "null" 769 | ], 770 | "type": "fill" 771 | } 772 | ], 773 | "measurement": "rain", 774 | "orderByTime": "ASC", 775 | "policy": "default", 776 | "refId": "B", 777 | "resultFormat": "time_series", 778 | "select": [ 779 | [ 780 | { 781 | "params": [ 782 | "fields_value" 783 | ], 784 | "type": "field" 785 | }, 786 | { 787 | "params": [], 788 | "type": "last" 789 | } 790 | ] 791 | ], 792 | "tags": [] 793 | } 794 | ], 795 | "title": "Rain", 796 | "type": "stat" 797 | }, 798 | { 799 | "collapsed": false, 800 | "datasource": null, 801 | "gridPos": { 802 | "h": 1, 803 | "w": 24, 804 | "x": 0, 805 | "y": 7 806 | }, 807 | "id": 108, 808 | "panels": [], 809 | "title": "Trend", 810 | "type": "row" 811 | }, 812 | { 813 | "aliasColors": {}, 814 | "bars": false, 815 | "dashLength": 10, 816 | "dashes": false, 817 | "datasource": null, 818 | "fieldConfig": { 819 | "defaults": { 820 | "custom": {}, 821 | "displayName": "temperature", 822 | "unit": "celsius" 823 | }, 824 | "overrides": [] 825 | }, 826 | "fill": 0, 827 | "fillGradient": 0, 828 | "gridPos": { 829 | "h": 8, 830 | "w": 10, 831 | "x": 0, 832 | "y": 8 833 | }, 834 | "hiddenSeries": false, 835 | "id": 200, 836 | "legend": { 837 | "alignAsTable": true, 838 | "avg": true, 839 | "current": true, 840 | "hideEmpty": true, 841 | "max": true, 842 | "min": true, 843 | "rightSide": false, 844 | "show": true, 845 | "total": false, 846 | "values": true 847 | }, 848 | "lines": true, 849 | "linewidth": 1, 850 | "links": [], 851 | "nullPointMode": "null", 852 | "options": { 853 | "alertThreshold": true 854 | }, 855 | "percentage": false, 856 | "pluginVersion": "7.3.10", 857 | "pointradius": 5, 858 | "points": false, 859 | "renderer": "flot", 860 | "seriesOverrides": [], 861 | "spaceLength": 10, 862 | "stack": false, 863 | "steppedLine": false, 864 | "targets": [ 865 | { 866 | "groupBy": [ 867 | { 868 | "params": [ 869 | "$__interval" 870 | ], 871 | "type": "time" 872 | }, 873 | { 874 | "params": [ 875 | "linear" 876 | ], 877 | "type": "fill" 878 | } 879 | ], 880 | "measurement": "humidity", 881 | "orderByTime": "ASC", 882 | "policy": "default", 883 | "refId": "B", 884 | "resultFormat": "time_series", 885 | "select": [ 886 | [ 887 | { 888 | "params": [ 889 | "fields_temperature" 890 | ], 891 | "type": "field" 892 | }, 893 | { 894 | "params": [], 895 | "type": "mean" 896 | } 897 | ] 898 | ], 899 | "tags": [] 900 | } 901 | ], 902 | "thresholds": [], 903 | "timeFrom": null, 904 | "timeRegions": [], 905 | "timeShift": null, 906 | "title": "Temperature", 907 | "tooltip": { 908 | "shared": true, 909 | "sort": 1, 910 | "value_type": "individual" 911 | }, 912 | "type": "graph", 913 | "xaxis": { 914 | "buckets": null, 915 | "mode": "time", 916 | "name": null, 917 | "show": true, 918 | "values": [] 919 | }, 920 | "yaxes": [ 921 | { 922 | "decimals": null, 923 | "format": "celsius", 924 | "label": "", 925 | "logBase": 1, 926 | "max": null, 927 | "min": null, 928 | "show": true 929 | }, 930 | { 931 | "format": "short", 932 | "label": null, 933 | "logBase": 1, 934 | "max": null, 935 | "min": null, 936 | "show": false 937 | } 938 | ], 939 | "yaxis": { 940 | "align": false, 941 | "alignLevel": null 942 | } 943 | }, 944 | { 945 | "aliasColors": {}, 946 | "bars": false, 947 | "dashLength": 10, 948 | "dashes": false, 949 | "datasource": null, 950 | "fieldConfig": { 951 | "defaults": { 952 | "custom": {}, 953 | "displayName": "wind speed" 954 | }, 955 | "overrides": [] 956 | }, 957 | "fill": 0, 958 | "fillGradient": 0, 959 | "gridPos": { 960 | "h": 8, 961 | "w": 10, 962 | "x": 10, 963 | "y": 8 964 | }, 965 | "hiddenSeries": false, 966 | "id": 202, 967 | "legend": { 968 | "alignAsTable": true, 969 | "avg": true, 970 | "current": true, 971 | "hideEmpty": true, 972 | "max": true, 973 | "min": true, 974 | "rightSide": false, 975 | "show": true, 976 | "total": false, 977 | "values": true 978 | }, 979 | "lines": true, 980 | "linewidth": 1, 981 | "links": [], 982 | "nullPointMode": "null", 983 | "options": { 984 | "alertThreshold": true 985 | }, 986 | "percentage": false, 987 | "pluginVersion": "7.3.10", 988 | "pointradius": 5, 989 | "points": false, 990 | "renderer": "flot", 991 | "seriesOverrides": [], 992 | "spaceLength": 10, 993 | "stack": false, 994 | "steppedLine": false, 995 | "targets": [ 996 | { 997 | "groupBy": [ 998 | { 999 | "params": [ 1000 | "$__interval" 1001 | ], 1002 | "type": "time" 1003 | }, 1004 | { 1005 | "params": [ 1006 | "linear" 1007 | ], 1008 | "type": "fill" 1009 | } 1010 | ], 1011 | "measurement": "wind-speed", 1012 | "orderByTime": "ASC", 1013 | "policy": "default", 1014 | "refId": "B", 1015 | "resultFormat": "time_series", 1016 | "select": [ 1017 | [ 1018 | { 1019 | "params": [ 1020 | "fields_value" 1021 | ], 1022 | "type": "field" 1023 | }, 1024 | { 1025 | "params": [], 1026 | "type": "mean" 1027 | } 1028 | ] 1029 | ], 1030 | "tags": [] 1031 | } 1032 | ], 1033 | "thresholds": [], 1034 | "timeFrom": null, 1035 | "timeRegions": [], 1036 | "timeShift": null, 1037 | "title": "Wind speed", 1038 | "tooltip": { 1039 | "shared": true, 1040 | "sort": 0, 1041 | "value_type": "individual" 1042 | }, 1043 | "type": "graph", 1044 | "xaxis": { 1045 | "buckets": null, 1046 | "mode": "time", 1047 | "name": null, 1048 | "show": true, 1049 | "values": [] 1050 | }, 1051 | "yaxes": [ 1052 | { 1053 | "format": "velocityms", 1054 | "label": null, 1055 | "logBase": 1, 1056 | "max": null, 1057 | "min": null, 1058 | "show": true 1059 | }, 1060 | { 1061 | "format": "short", 1062 | "label": null, 1063 | "logBase": 1, 1064 | "max": null, 1065 | "min": null, 1066 | "show": false 1067 | } 1068 | ], 1069 | "yaxis": { 1070 | "align": false, 1071 | "alignLevel": null 1072 | } 1073 | }, 1074 | { 1075 | "aliasColors": {}, 1076 | "bars": false, 1077 | "dashLength": 10, 1078 | "dashes": false, 1079 | "datasource": null, 1080 | "decimals": 0, 1081 | "fieldConfig": { 1082 | "defaults": { 1083 | "custom": {}, 1084 | "displayName": "rain in mm/d" 1085 | }, 1086 | "overrides": [] 1087 | }, 1088 | "fill": 0, 1089 | "fillGradient": 0, 1090 | "gridPos": { 1091 | "h": 7, 1092 | "w": 10, 1093 | "x": 0, 1094 | "y": 16 1095 | }, 1096 | "hiddenSeries": false, 1097 | "id": 206, 1098 | "legend": { 1099 | "alignAsTable": true, 1100 | "avg": true, 1101 | "current": true, 1102 | "hideEmpty": true, 1103 | "max": true, 1104 | "min": true, 1105 | "rightSide": false, 1106 | "show": true, 1107 | "total": false, 1108 | "values": true 1109 | }, 1110 | "lines": true, 1111 | "linewidth": 1, 1112 | "links": [], 1113 | "nullPointMode": "null", 1114 | "options": { 1115 | "alertThreshold": true 1116 | }, 1117 | "percentage": false, 1118 | "pluginVersion": "7.3.10", 1119 | "pointradius": 5, 1120 | "points": false, 1121 | "renderer": "flot", 1122 | "seriesOverrides": [], 1123 | "spaceLength": 10, 1124 | "stack": false, 1125 | "steppedLine": false, 1126 | "targets": [ 1127 | { 1128 | "groupBy": [ 1129 | { 1130 | "params": [ 1131 | "$__interval" 1132 | ], 1133 | "type": "time" 1134 | }, 1135 | { 1136 | "params": [ 1137 | "null" 1138 | ], 1139 | "type": "fill" 1140 | } 1141 | ], 1142 | "measurement": "rain", 1143 | "orderByTime": "ASC", 1144 | "policy": "default", 1145 | "refId": "B", 1146 | "resultFormat": "time_series", 1147 | "select": [ 1148 | [ 1149 | { 1150 | "params": [ 1151 | "fields_value" 1152 | ], 1153 | "type": "field" 1154 | }, 1155 | { 1156 | "params": [], 1157 | "type": "distinct" 1158 | } 1159 | ] 1160 | ], 1161 | "tags": [] 1162 | } 1163 | ], 1164 | "thresholds": [], 1165 | "timeFrom": null, 1166 | "timeRegions": [], 1167 | "timeShift": null, 1168 | "title": "Rain", 1169 | "tooltip": { 1170 | "shared": true, 1171 | "sort": 0, 1172 | "value_type": "individual" 1173 | }, 1174 | "type": "graph", 1175 | "xaxis": { 1176 | "buckets": null, 1177 | "mode": "time", 1178 | "name": null, 1179 | "show": true, 1180 | "values": [] 1181 | }, 1182 | "yaxes": [ 1183 | { 1184 | "decimals": 0, 1185 | "format": "lengthmm", 1186 | "label": null, 1187 | "logBase": 1, 1188 | "max": null, 1189 | "min": "0", 1190 | "show": true 1191 | }, 1192 | { 1193 | "format": "short", 1194 | "label": null, 1195 | "logBase": 1, 1196 | "max": null, 1197 | "min": null, 1198 | "show": false 1199 | } 1200 | ], 1201 | "yaxis": { 1202 | "align": false, 1203 | "alignLevel": null 1204 | } 1205 | }, 1206 | { 1207 | "aliasColors": {}, 1208 | "bars": false, 1209 | "dashLength": 10, 1210 | "dashes": false, 1211 | "datasource": null, 1212 | "decimals": 0, 1213 | "fieldConfig": { 1214 | "defaults": { 1215 | "custom": {}, 1216 | "displayName": "humidity" 1217 | }, 1218 | "overrides": [] 1219 | }, 1220 | "fill": 0, 1221 | "fillGradient": 0, 1222 | "gridPos": { 1223 | "h": 7, 1224 | "w": 10, 1225 | "x": 10, 1226 | "y": 16 1227 | }, 1228 | "hiddenSeries": false, 1229 | "id": 205, 1230 | "legend": { 1231 | "alignAsTable": true, 1232 | "avg": true, 1233 | "current": true, 1234 | "hideEmpty": true, 1235 | "max": true, 1236 | "min": true, 1237 | "rightSide": false, 1238 | "show": true, 1239 | "total": false, 1240 | "values": true 1241 | }, 1242 | "lines": true, 1243 | "linewidth": 1, 1244 | "links": [], 1245 | "nullPointMode": "null", 1246 | "options": { 1247 | "alertThreshold": true 1248 | }, 1249 | "percentage": false, 1250 | "pluginVersion": "7.3.10", 1251 | "pointradius": 5, 1252 | "points": false, 1253 | "renderer": "flot", 1254 | "seriesOverrides": [], 1255 | "spaceLength": 10, 1256 | "stack": false, 1257 | "steppedLine": false, 1258 | "targets": [ 1259 | { 1260 | "groupBy": [ 1261 | { 1262 | "params": [ 1263 | "$__interval" 1264 | ], 1265 | "type": "time" 1266 | }, 1267 | { 1268 | "params": [ 1269 | "none" 1270 | ], 1271 | "type": "fill" 1272 | } 1273 | ], 1274 | "measurement": "humidity", 1275 | "orderByTime": "ASC", 1276 | "policy": "default", 1277 | "refId": "B", 1278 | "resultFormat": "time_series", 1279 | "select": [ 1280 | [ 1281 | { 1282 | "params": [ 1283 | "fields_humidity" 1284 | ], 1285 | "type": "field" 1286 | }, 1287 | { 1288 | "params": [], 1289 | "type": "mean" 1290 | } 1291 | ] 1292 | ], 1293 | "tags": [] 1294 | } 1295 | ], 1296 | "thresholds": [], 1297 | "timeFrom": null, 1298 | "timeRegions": [], 1299 | "timeShift": null, 1300 | "title": "Humidity", 1301 | "tooltip": { 1302 | "shared": true, 1303 | "sort": 0, 1304 | "value_type": "individual" 1305 | }, 1306 | "type": "graph", 1307 | "xaxis": { 1308 | "buckets": null, 1309 | "mode": "time", 1310 | "name": null, 1311 | "show": true, 1312 | "values": [] 1313 | }, 1314 | "yaxes": [ 1315 | { 1316 | "decimals": 0, 1317 | "format": "percent", 1318 | "label": null, 1319 | "logBase": 1, 1320 | "max": null, 1321 | "min": null, 1322 | "show": true 1323 | }, 1324 | { 1325 | "format": "short", 1326 | "label": null, 1327 | "logBase": 1, 1328 | "max": null, 1329 | "min": null, 1330 | "show": false 1331 | } 1332 | ], 1333 | "yaxis": { 1334 | "align": false, 1335 | "alignLevel": null 1336 | } 1337 | } 1338 | ], 1339 | "refresh": "10s", 1340 | "schemaVersion": 26, 1341 | "style": "dark", 1342 | "tags": [], 1343 | "templating": { 1344 | "list": [] 1345 | }, 1346 | "time": { 1347 | "from": "now-2d", 1348 | "to": "now" 1349 | }, 1350 | "timepicker": { 1351 | "refresh_intervals": [ 1352 | "5s", 1353 | "10s", 1354 | "30s", 1355 | "1m", 1356 | "5m", 1357 | "15m", 1358 | "30m", 1359 | "1h", 1360 | "2h", 1361 | "1d" 1362 | ], 1363 | "time_options": [ 1364 | "5m", 1365 | "15m", 1366 | "1h", 1367 | "6h", 1368 | "12h", 1369 | "24h", 1370 | "2d", 1371 | "7d", 1372 | "30d" 1373 | ] 1374 | }, 1375 | "timezone": "", 1376 | "title": "Weather Map", 1377 | "uid": "7wISvn_iz", 1378 | "version": 30 1379 | } 1380 | -------------------------------------------------------------------------------- /mqtt/mosquitto.conf: -------------------------------------------------------------------------------- 1 | # Config file for mosquitto 2 | # 3 | # See mosquitto.conf(5) for more information. 4 | # 5 | # Default values are shown, uncomment to change. 6 | # 7 | # Use the # character to indicate a comment, but only if it is the 8 | # very first character on the line. 9 | 10 | # ================================================================= 11 | # General configuration 12 | # ================================================================= 13 | 14 | # Use per listener security settings. 15 | # 16 | # It is recommended this option be set before any other options. 17 | # 18 | # If this option is set to true, then all authentication and access control 19 | # options are controlled on a per listener basis. The following options are 20 | # affected: 21 | # 22 | # password_file acl_file psk_file auth_plugin auth_opt_* allow_anonymous 23 | # auto_id_prefix allow_zero_length_clientid 24 | # 25 | # Note that if set to true, then a durable client (i.e. with clean session set 26 | # to false) that has disconnected will use the ACL settings defined for the 27 | # listener that it was most recently connected to. 28 | # 29 | # The default behaviour is for this to be set to false, which maintains the 30 | # setting behaviour from previous versions of mosquitto. 31 | # per_listener_settings true 32 | 33 | 34 | # This option controls whether a client is allowed to connect with a zero 35 | # length client id or not. This option only affects clients using MQTT v3.1.1 36 | # and later. If set to false, clients connecting with a zero length client id 37 | # are disconnected. If set to true, clients will be allocated a client id by 38 | # the broker. This means it is only useful for clients with clean session set 39 | # to true. 40 | #allow_zero_length_clientid true 41 | 42 | # If allow_zero_length_clientid is true, this option allows you to set a prefix 43 | # to automatically generated client ids to aid visibility in logs. 44 | # Defaults to 'auto-' 45 | #auto_id_prefix auto- 46 | 47 | # This option affects the scenario when a client subscribes to a topic that has 48 | # retained messages. It is possible that the client that published the retained 49 | # message to the topic had access at the time they published, but that access 50 | # has been subsequently removed. If check_retain_source is set to true, the 51 | # default, the source of a retained message will be checked for access rights 52 | # before it is republished. When set to false, no check will be made and the 53 | # retained message will always be published. This affects all listeners. 54 | #check_retain_source true 55 | 56 | # QoS 1 and 2 messages will be allowed inflight per client until this limit 57 | # is exceeded. Defaults to 0. (No maximum) 58 | # See also max_inflight_messages 59 | #max_inflight_bytes 0 60 | 61 | # The maximum number of QoS 1 and 2 messages currently inflight per 62 | # client. 63 | # This includes messages that are partway through handshakes and 64 | # those that are being retried. Defaults to 20. Set to 0 for no 65 | # maximum. Setting to 1 will guarantee in-order delivery of QoS 1 66 | # and 2 messages. 67 | #max_inflight_messages 20 68 | 69 | # For MQTT v5 clients, it is possible to have the server send a "server 70 | # keepalive" value that will override the keepalive value set by the client. 71 | # This is intended to be used as a mechanism to say that the server will 72 | # disconnect the client earlier than it anticipated, and that the client should 73 | # use the new keepalive value. The max_keepalive option allows you to specify 74 | # that clients may only connect with keepalive less than or equal to this 75 | # value, otherwise they will be sent a server keepalive telling them to use 76 | # max_keepalive. This only applies to MQTT v5 clients. The maximum value 77 | # allowable is 65535. Do not set below 10. 78 | #max_keepalive 65535 79 | 80 | # For MQTT v5 clients, it is possible to have the server send a "maximum packet 81 | # size" value that will instruct the client it will not accept MQTT packets 82 | # with size greater than max_packet_size bytes. This applies to the full MQTT 83 | # packet, not just the payload. Setting this option to a positive value will 84 | # set the maximum packet size to that number of bytes. If a client sends a 85 | # packet which is larger than this value, it will be disconnected. This applies 86 | # to all clients regardless of the protocol version they are using, but v3.1.1 87 | # and earlier clients will of course not have received the maximum packet size 88 | # information. Defaults to no limit. Setting below 20 bytes is forbidden 89 | # because it is likely to interfere with ordinary client operation, even with 90 | # very small payloads. 91 | #max_packet_size 0 92 | 93 | # QoS 1 and 2 messages above those currently in-flight will be queued per 94 | # client until this limit is exceeded. Defaults to 0. (No maximum) 95 | # See also max_queued_messages. 96 | # If both max_queued_messages and max_queued_bytes are specified, packets will 97 | # be queued until the first limit is reached. 98 | #max_queued_bytes 0 99 | 100 | # Set the maximum QoS supported. Clients publishing at a QoS higher than 101 | # specified here will be disconnected. 102 | #max_qos 2 103 | 104 | # The maximum number of QoS 1 and 2 messages to hold in a queue per client 105 | # above those that are currently in-flight. Defaults to 1000. Set 106 | # to 0 for no maximum (not recommended). 107 | # See also queue_qos0_messages. 108 | # See also max_queued_bytes. 109 | #max_queued_messages 1000 110 | # 111 | # This option sets the maximum number of heap memory bytes that the broker will 112 | # allocate, and hence sets a hard limit on memory use by the broker. Memory 113 | # requests that exceed this value will be denied. The effect will vary 114 | # depending on what has been denied. If an incoming message is being processed, 115 | # then the message will be dropped and the publishing client will be 116 | # disconnected. If an outgoing message is being sent, then the individual 117 | # message will be dropped and the receiving client will be disconnected. 118 | # Defaults to no limit. 119 | #memory_limit 0 120 | 121 | # This option sets the maximum publish payload size that the broker will allow. 122 | # Received messages that exceed this size will not be accepted by the broker. 123 | # The default value is 0, which means that all valid MQTT messages are 124 | # accepted. MQTT imposes a maximum payload size of 268435455 bytes. 125 | #message_size_limit 0 126 | 127 | # This option allows persistent clients (those with clean session set to false) 128 | # to be removed if they do not reconnect within a certain time frame. 129 | # 130 | # This is a non-standard option in MQTT V3.1 but allowed in MQTT v3.1.1. 131 | # 132 | # Badly designed clients may set clean session to false whilst using a randomly 133 | # generated client id. This leads to persistent clients that will never 134 | # reconnect. This option allows these clients to be removed. 135 | # 136 | # The expiration period should be an integer followed by one of h d w m y for 137 | # hour, day, week, month and year respectively. For example 138 | # 139 | # persistent_client_expiration 2m 140 | # persistent_client_expiration 14d 141 | # persistent_client_expiration 1y 142 | # 143 | # The default if not set is to never expire persistent clients. 144 | #persistent_client_expiration 145 | 146 | # Write process id to a file. Default is a blank string which means 147 | # a pid file shouldn't be written. 148 | # This should be set to /var/run/mosquitto/mosquitto.pid if mosquitto is 149 | # being run automatically on boot with an init script and 150 | # start-stop-daemon or similar. 151 | #pid_file 152 | 153 | # Set to true to queue messages with QoS 0 when a persistent client is 154 | # disconnected. These messages are included in the limit imposed by 155 | # max_queued_messages and max_queued_bytes 156 | # Defaults to false. 157 | # This is a non-standard option for the MQTT v3.1 spec but is allowed in 158 | # v3.1.1. 159 | #queue_qos0_messages false 160 | 161 | # Set to false to disable retained message support. If a client publishes a 162 | # message with the retain bit set, it will be disconnected if this is set to 163 | # false. 164 | #retain_available true 165 | 166 | # Disable Nagle's algorithm on client sockets. This has the effect of reducing 167 | # latency of individual messages at the potential cost of increasing the number 168 | # of packets being sent. 169 | #set_tcp_nodelay false 170 | 171 | # Time in seconds between updates of the $SYS tree. 172 | # Set to 0 to disable the publishing of the $SYS tree. 173 | #sys_interval 10 174 | 175 | # The MQTT specification requires that the QoS of a message delivered to a 176 | # subscriber is never upgraded to match the QoS of the subscription. Enabling 177 | # this option changes this behaviour. If upgrade_outgoing_qos is set true, 178 | # messages sent to a subscriber will always match the QoS of its subscription. 179 | # This is a non-standard option explicitly disallowed by the spec. 180 | #upgrade_outgoing_qos false 181 | 182 | # When run as root, drop privileges to this user and its primary 183 | # group. 184 | # Set to root to stay as root, but this is not recommended. 185 | # If set to "mosquitto", or left unset, and the "mosquitto" user does not exist 186 | # then it will drop privileges to the "nobody" user instead. 187 | # If run as a non-root user, this setting has no effect. 188 | # Note that on Windows this has no effect and so mosquitto should be started by 189 | # the user you wish it to run as. 190 | #user mosquitto 191 | 192 | # ================================================================= 193 | # Listeners 194 | # ================================================================= 195 | 196 | # Listen on a port/ip address combination. By using this variable 197 | # multiple times, mosquitto can listen on more than one port. If 198 | # this variable is used and neither bind_address nor port given, 199 | # then the default listener will not be started. 200 | # The port number to listen on must be given. Optionally, an ip 201 | # address or host name may be supplied as a second argument. In 202 | # this case, mosquitto will attempt to bind the listener to that 203 | # address and so restrict access to the associated network and 204 | # interface. By default, mosquitto will listen on all interfaces. 205 | # Note that for a websockets listener it is not possible to bind to a host 206 | # name. 207 | # 208 | # On systems that support Unix Domain Sockets, it is also possible 209 | # to create a # Unix socket rather than opening a TCP socket. In 210 | # this case, the port number should be set to 0 and a unix socket 211 | # path must be provided, e.g. 212 | # listener 0 /tmp/mosquitto.sock 213 | # 214 | # listener port-number [ip address/host name/unix socket path] 215 | listener 1883 216 | protocol mqtt 217 | 218 | listener 9001 219 | protocol websockets 220 | 221 | # By default, a listener will attempt to listen on all supported IP protocol 222 | # versions. If you do not have an IPv4 or IPv6 interface you may wish to 223 | # disable support for either of those protocol versions. In particular, note 224 | # that due to the limitations of the websockets library, it will only ever 225 | # attempt to open IPv6 sockets if IPv6 support is compiled in, and so will fail 226 | # if IPv6 is not available. 227 | # 228 | # Set to `ipv4` to force the listener to only use IPv4, or set to `ipv6` to 229 | # force the listener to only use IPv6. If you want support for both IPv4 and 230 | # IPv6, then do not use the socket_domain option. 231 | # 232 | #socket_domain 233 | 234 | # Bind the listener to a specific interface. This is similar to 235 | # the [ip address/host name] part of the listener definition, but is useful 236 | # when an interface has multiple addresses or the address may change. If used 237 | # with the [ip address/host name] part of the listener definition, then the 238 | # bind_interface option will take priority. 239 | # Not available on Windows. 240 | # 241 | # Example: bind_interface eth0 242 | bind_interface eth0 243 | 244 | # When a listener is using the websockets protocol, it is possible to serve 245 | # http data as well. Set http_dir to a directory which contains the files you 246 | # wish to serve. If this option is not specified, then no normal http 247 | # connections will be possible. 248 | #http_dir 249 | 250 | # The maximum number of client connections to allow. This is 251 | # a per listener setting. 252 | # Default is -1, which means unlimited connections. 253 | # Note that other process limits mean that unlimited connections 254 | # are not really possible. Typically the default maximum number of 255 | # connections possible is around 1024. 256 | #max_connections -1 257 | 258 | # The listener can be restricted to operating within a topic hierarchy using 259 | # the mount_point option. This is achieved be prefixing the mount_point string 260 | # to all topics for any clients connected to this listener. This prefixing only 261 | # happens internally to the broker; the client will not see the prefix. 262 | #mount_point 263 | 264 | # Choose the protocol to use when listening. 265 | # This can be either mqtt or websockets. 266 | # Certificate based TLS may be used with websockets, except that only the 267 | # cafile, certfile, keyfile, ciphers, and ciphers_tls13 options are supported. 268 | #protocol mqtt 269 | 270 | # Set use_username_as_clientid to true to replace the clientid that a client 271 | # connected with with its username. This allows authentication to be tied to 272 | # the clientid, which means that it is possible to prevent one client 273 | # disconnecting another by using the same clientid. 274 | # If a client connects with no username it will be disconnected as not 275 | # authorised when this option is set to true. 276 | # Do not use in conjunction with clientid_prefixes. 277 | # See also use_identity_as_username. 278 | #use_username_as_clientid 279 | 280 | # Change the websockets headers size. This is a global option, it is not 281 | # possible to set per listener. This option sets the size of the buffer used in 282 | # the libwebsockets library when reading HTTP headers. If you are passing large 283 | # header data such as cookies then you may need to increase this value. If left 284 | # unset, or set to 0, then the default of 1024 bytes will be used. 285 | #websockets_headers_size 286 | 287 | # ----------------------------------------------------------------- 288 | # Certificate based SSL/TLS support 289 | # ----------------------------------------------------------------- 290 | # The following options can be used to enable certificate based SSL/TLS support 291 | # for this listener. Note that the recommended port for MQTT over TLS is 8883, 292 | # but this must be set manually. 293 | # 294 | # See also the mosquitto-tls man page and the "Pre-shared-key based SSL/TLS 295 | # support" section. Only one of certificate or PSK encryption support can be 296 | # enabled for any listener. 297 | 298 | # Both of certfile and keyfile must be defined to enable certificate based 299 | # TLS encryption. 300 | 301 | # Path to the PEM encoded server certificate. 302 | #certfile 303 | 304 | # Path to the PEM encoded keyfile. 305 | #keyfile 306 | 307 | # If you wish to control which encryption ciphers are used, use the ciphers 308 | # option. The list of available ciphers can be optained using the "openssl 309 | # ciphers" command and should be provided in the same format as the output of 310 | # that command. This applies to TLS 1.2 and earlier versions only. Use 311 | # ciphers_tls1.3 for TLS v1.3. 312 | #ciphers 313 | 314 | # Choose which TLS v1.3 ciphersuites are used for this listener. 315 | # Defaults to "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256" 316 | #ciphers_tls1.3 317 | 318 | # If you have require_certificate set to true, you can create a certificate 319 | # revocation list file to revoke access to particular client certificates. If 320 | # you have done this, use crlfile to point to the PEM encoded revocation file. 321 | #crlfile 322 | 323 | # To allow the use of ephemeral DH key exchange, which provides forward 324 | # security, the listener must load DH parameters. This can be specified with 325 | # the dhparamfile option. The dhparamfile can be generated with the command 326 | # e.g. "openssl dhparam -out dhparam.pem 2048" 327 | #dhparamfile 328 | 329 | # By default an TLS enabled listener will operate in a similar fashion to a 330 | # https enabled web server, in that the server has a certificate signed by a CA 331 | # and the client will verify that it is a trusted certificate. The overall aim 332 | # is encryption of the network traffic. By setting require_certificate to true, 333 | # the client must provide a valid certificate in order for the network 334 | # connection to proceed. This allows access to the broker to be controlled 335 | # outside of the mechanisms provided by MQTT. 336 | #require_certificate false 337 | 338 | # cafile and capath define methods of accessing the PEM encoded 339 | # Certificate Authority certificates that will be considered trusted when 340 | # checking incoming client certificates. 341 | # cafile defines the path to a file containing the CA certificates. 342 | # capath defines a directory that will be searched for files 343 | # containing the CA certificates. For capath to work correctly, the 344 | # certificate files must have ".crt" as the file ending and you must run 345 | # "openssl rehash " each time you add/remove a certificate. 346 | #cafile 347 | #capath 348 | 349 | 350 | # If require_certificate is true, you may set use_identity_as_username to true 351 | # to use the CN value from the client certificate as a username. If this is 352 | # true, the password_file option will not be used for this listener. 353 | #use_identity_as_username false 354 | 355 | # ----------------------------------------------------------------- 356 | # Pre-shared-key based SSL/TLS support 357 | # ----------------------------------------------------------------- 358 | # The following options can be used to enable PSK based SSL/TLS support for 359 | # this listener. Note that the recommended port for MQTT over TLS is 8883, but 360 | # this must be set manually. 361 | # 362 | # See also the mosquitto-tls man page and the "Certificate based SSL/TLS 363 | # support" section. Only one of certificate or PSK encryption support can be 364 | # enabled for any listener. 365 | 366 | # The psk_hint option enables pre-shared-key support for this listener and also 367 | # acts as an identifier for this listener. The hint is sent to clients and may 368 | # be used locally to aid authentication. The hint is a free form string that 369 | # doesn't have much meaning in itself, so feel free to be creative. 370 | # If this option is provided, see psk_file to define the pre-shared keys to be 371 | # used or create a security plugin to handle them. 372 | #psk_hint 373 | 374 | # When using PSK, the encryption ciphers used will be chosen from the list of 375 | # available PSK ciphers. If you want to control which ciphers are available, 376 | # use the "ciphers" option. The list of available ciphers can be optained 377 | # using the "openssl ciphers" command and should be provided in the same format 378 | # as the output of that command. 379 | #ciphers 380 | 381 | # Set use_identity_as_username to have the psk identity sent by the client used 382 | # as its username. Authentication will be carried out using the PSK rather than 383 | # the MQTT username/password and so password_file will not be used for this 384 | # listener. 385 | #use_identity_as_username false 386 | 387 | 388 | # ================================================================= 389 | # Persistence 390 | # ================================================================= 391 | 392 | # If persistence is enabled, save the in-memory database to disk 393 | # every autosave_interval seconds. If set to 0, the persistence 394 | # database will only be written when mosquitto exits. See also 395 | # autosave_on_changes. 396 | # Note that writing of the persistence database can be forced by 397 | # sending mosquitto a SIGUSR1 signal. 398 | #autosave_interval 1800 399 | 400 | # If true, mosquitto will count the number of subscription changes, retained 401 | # messages received and queued messages and if the total exceeds 402 | # autosave_interval then the in-memory database will be saved to disk. 403 | # If false, mosquitto will save the in-memory database to disk by treating 404 | # autosave_interval as a time in seconds. 405 | #autosave_on_changes false 406 | 407 | # Save persistent message data to disk (true/false). 408 | # This saves information about all messages, including 409 | # subscriptions, currently in-flight messages and retained 410 | # messages. 411 | # retained_persistence is a synonym for this option. 412 | #persistence false 413 | 414 | # The filename to use for the persistent database, not including 415 | # the path. 416 | #persistence_file mosquitto.db 417 | 418 | # Location for persistent database. 419 | # Default is an empty string (current directory). 420 | # Set to e.g. /var/lib/mosquitto if running as a proper service on Linux or 421 | # similar. 422 | #persistence_location 423 | 424 | 425 | # ================================================================= 426 | # Logging 427 | # ================================================================= 428 | 429 | # Places to log to. Use multiple log_dest lines for multiple 430 | # logging destinations. 431 | # Possible destinations are: stdout stderr syslog topic file dlt 432 | # 433 | # stdout and stderr log to the console on the named output. 434 | # 435 | # syslog uses the userspace syslog facility which usually ends up 436 | # in /var/log/messages or similar. 437 | # 438 | # topic logs to the broker topic '$SYS/broker/log/', 439 | # where severity is one of D, E, W, N, I, M which are debug, error, 440 | # warning, notice, information and message. Message type severity is used by 441 | # the subscribe/unsubscribe log_types and publishes log messages to 442 | # $SYS/broker/log/M/susbcribe or $SYS/broker/log/M/unsubscribe. 443 | # 444 | # The file destination requires an additional parameter which is the file to be 445 | # logged to, e.g. "log_dest file /var/log/mosquitto.log". The file will be 446 | # closed and reopened when the broker receives a HUP signal. Only a single file 447 | # destination may be configured. 448 | # 449 | # The dlt destination is for the automotive `Diagnostic Log and Trace` tool. 450 | # This requires that Mosquitto has been compiled with DLT support. 451 | # 452 | # Note that if the broker is running as a Windows service it will default to 453 | # "log_dest none" and neither stdout nor stderr logging is available. 454 | # Use "log_dest none" if you wish to disable logging. 455 | #log_dest stderr 456 | 457 | # Types of messages to log. Use multiple log_type lines for logging 458 | # multiple types of messages. 459 | # Possible types are: debug, error, warning, notice, information, 460 | # none, subscribe, unsubscribe, websockets, all. 461 | # Note that debug type messages are for decoding the incoming/outgoing 462 | # network packets. They are not logged in "topics". 463 | #log_type error 464 | #log_type warning 465 | #log_type notice 466 | #log_type information 467 | 468 | 469 | # If set to true, client connection and disconnection messages will be included 470 | # in the log. 471 | #connection_messages true 472 | 473 | # If using syslog logging (not on Windows), messages will be logged to the 474 | # "daemon" facility by default. Use the log_facility option to choose which of 475 | # local0 to local7 to log to instead. The option value should be an integer 476 | # value, e.g. "log_facility 5" to use local5. 477 | #log_facility 478 | 479 | # If set to true, add a timestamp value to each log message. 480 | #log_timestamp true 481 | 482 | # Set the format of the log timestamp. If left unset, this is the number of 483 | # seconds since the Unix epoch. 484 | # This is a free text string which will be passed to the strftime function. To 485 | # get an ISO 8601 datetime, for example: 486 | # log_timestamp_format %Y-%m-%dT%H:%M:%S 487 | #log_timestamp_format 488 | 489 | # Change the websockets logging level. This is a global option, it is not 490 | # possible to set per listener. This is an integer that is interpreted by 491 | # libwebsockets as a bit mask for its lws_log_levels enum. See the 492 | # libwebsockets documentation for more details. "log_type websockets" must also 493 | # be enabled. 494 | #websockets_log_level 0 495 | 496 | 497 | # ================================================================= 498 | # Security 499 | # ================================================================= 500 | 501 | # If set, only clients that have a matching prefix on their 502 | # clientid will be allowed to connect to the broker. By default, 503 | # all clients may connect. 504 | # For example, setting "secure-" here would mean a client "secure- 505 | # client" could connect but another with clientid "mqtt" couldn't. 506 | #clientid_prefixes 507 | 508 | # Boolean value that determines whether clients that connect 509 | # without providing a username are allowed to connect. If set to 510 | # false then a password file should be created (see the 511 | # password_file option) to control authenticated client access. 512 | # 513 | # Defaults to false, unless there are no listeners defined in the configuration 514 | # file, in which case it is set to true, but connections are only allowed from 515 | # the local machine. 516 | allow_anonymous true 517 | 518 | # ----------------------------------------------------------------- 519 | # Default authentication and topic access control 520 | # ----------------------------------------------------------------- 521 | 522 | # Control access to the broker using a password file. This file can be 523 | # generated using the mosquitto_passwd utility. If TLS support is not compiled 524 | # into mosquitto (it is recommended that TLS support should be included) then 525 | # plain text passwords are used, in which case the file should be a text file 526 | # with lines in the format: 527 | # username:password 528 | # The password (and colon) may be omitted if desired, although this 529 | # offers very little in the way of security. 530 | # 531 | # See the TLS client require_certificate and use_identity_as_username options 532 | # for alternative authentication options. If an auth_plugin is used as well as 533 | # password_file, the auth_plugin check will be made first. 534 | #password_file 535 | 536 | # Access may also be controlled using a pre-shared-key file. This requires 537 | # TLS-PSK support and a listener configured to use it. The file should be text 538 | # lines in the format: 539 | # identity:key 540 | # The key should be in hexadecimal format without a leading "0x". 541 | # If an auth_plugin is used as well, the auth_plugin check will be made first. 542 | #psk_file 543 | 544 | # Control access to topics on the broker using an access control list 545 | # file. If this parameter is defined then only the topics listed will 546 | # have access. 547 | # If the first character of a line of the ACL file is a # it is treated as a 548 | # comment. 549 | # Topic access is added with lines of the format: 550 | # 551 | # topic [read|write|readwrite|deny] 552 | # 553 | # The access type is controlled using "read", "write", "readwrite" or "deny". 554 | # This parameter is optional (unless contains a space character) - if 555 | # not given then the access is read/write. can contain the + or # 556 | # wildcards as in subscriptions. 557 | # 558 | # The "deny" option can used to explicity deny access to a topic that would 559 | # otherwise be granted by a broader read/write/readwrite statement. Any "deny" 560 | # topics are handled before topics that grant read/write access. 561 | # 562 | # The first set of topics are applied to anonymous clients, assuming 563 | # allow_anonymous is true. User specific topic ACLs are added after a 564 | # user line as follows: 565 | # 566 | # user 567 | # 568 | # The username referred to here is the same as in password_file. It is 569 | # not the clientid. 570 | # 571 | # 572 | # If is also possible to define ACLs based on pattern substitution within the 573 | # topic. The patterns available for substition are: 574 | # 575 | # %c to match the client id of the client 576 | # %u to match the username of the client 577 | # 578 | # The substitution pattern must be the only text for that level of hierarchy. 579 | # 580 | # The form is the same as for the topic keyword, but using pattern as the 581 | # keyword. 582 | # Pattern ACLs apply to all users even if the "user" keyword has previously 583 | # been given. 584 | # 585 | # If using bridges with usernames and ACLs, connection messages can be allowed 586 | # with the following pattern: 587 | # pattern write $SYS/broker/connection/%c/state 588 | # 589 | # pattern [read|write|readwrite] 590 | # 591 | # Example: 592 | # 593 | # pattern write sensor/%u/data 594 | # 595 | # If an auth_plugin is used as well as acl_file, the auth_plugin check will be 596 | # made first. 597 | #acl_file 598 | 599 | # ----------------------------------------------------------------- 600 | # External authentication and topic access plugin options 601 | # ----------------------------------------------------------------- 602 | 603 | # External authentication and access control can be supported with the 604 | # auth_plugin option. This is a path to a loadable plugin. See also the 605 | # auth_opt_* options described below. 606 | # 607 | # The auth_plugin option can be specified multiple times to load multiple 608 | # plugins. The plugins will be processed in the order that they are specified 609 | # here. If the auth_plugin option is specified alongside either of 610 | # password_file or acl_file then the plugin checks will be made first. 611 | # 612 | #auth_plugin 613 | 614 | # If the auth_plugin option above is used, define options to pass to the 615 | # plugin here as described by the plugin instructions. All options named 616 | # using the format auth_opt_* will be passed to the plugin, for example: 617 | # 618 | # auth_opt_db_host 619 | # auth_opt_db_port 620 | # auth_opt_db_username 621 | # auth_opt_db_password 622 | 623 | 624 | # ================================================================= 625 | # Bridges 626 | # ================================================================= 627 | 628 | # A bridge is a way of connecting multiple MQTT brokers together. 629 | # Create a new bridge using the "connection" option as described below. Set 630 | # options for the bridges using the remaining parameters. You must specify the 631 | # address and at least one topic to subscribe to. 632 | # 633 | # Each connection must have a unique name. 634 | # 635 | # The address line may have multiple host address and ports specified. See 636 | # below in the round_robin description for more details on bridge behaviour if 637 | # multiple addresses are used. Note that if you use an IPv6 address, then you 638 | # are required to specify a port. 639 | # 640 | # The direction that the topic will be shared can be chosen by 641 | # specifying out, in or both, where the default value is out. 642 | # The QoS level of the bridged communication can be specified with the next 643 | # topic option. The default QoS level is 0, to change the QoS the topic 644 | # direction must also be given. 645 | # 646 | # The local and remote prefix options allow a topic to be remapped when it is 647 | # bridged to/from the remote broker. This provides the ability to place a topic 648 | # tree in an appropriate location. 649 | # 650 | # For more details see the mosquitto.conf man page. 651 | # 652 | # Multiple topics can be specified per connection, but be careful 653 | # not to create any loops. 654 | # 655 | # If you are using bridges with cleansession set to false (the default), then 656 | # you may get unexpected behaviour from incoming topics if you change what 657 | # topics you are subscribing to. This is because the remote broker keeps the 658 | # subscription for the old topic. If you have this problem, connect your bridge 659 | # with cleansession set to true, then reconnect with cleansession set to false 660 | # as normal. 661 | #connection 662 | #address [:] [[:]] 663 | #topic [[[out | in | both] qos-level] local-prefix remote-prefix] 664 | 665 | # If you need to have the bridge connect over a particular network interface, 666 | # use bridge_bind_address to tell the bridge which local IP address the socket 667 | # should bind to, e.g. `bridge_bind_address 192.168.1.10` 668 | #bridge_bind_address 669 | 670 | # If a bridge has topics that have "out" direction, the default behaviour is to 671 | # send an unsubscribe request to the remote broker on that topic. This means 672 | # that changing a topic direction from "in" to "out" will not keep receiving 673 | # incoming messages. Sending these unsubscribe requests is not always 674 | # desirable, setting bridge_attempt_unsubscribe to false will disable sending 675 | # the unsubscribe request. 676 | #bridge_attempt_unsubscribe true 677 | 678 | # Set the version of the MQTT protocol to use with for this bridge. Can be one 679 | # of mqttv50, mqttv311 or mqttv31. Defaults to mqttv311. 680 | #bridge_protocol_version mqttv311 681 | 682 | # Set the clean session variable for this bridge. 683 | # When set to true, when the bridge disconnects for any reason, all 684 | # messages and subscriptions will be cleaned up on the remote 685 | # broker. Note that with cleansession set to true, there may be a 686 | # significant amount of retained messages sent when the bridge 687 | # reconnects after losing its connection. 688 | # When set to false, the subscriptions and messages are kept on the 689 | # remote broker, and delivered when the bridge reconnects. 690 | #cleansession false 691 | 692 | # Set the amount of time a bridge using the lazy start type must be idle before 693 | # it will be stopped. Defaults to 60 seconds. 694 | #idle_timeout 60 695 | 696 | # Set the keepalive interval for this bridge connection, in 697 | # seconds. 698 | #keepalive_interval 60 699 | 700 | # Set the clientid to use on the local broker. If not defined, this defaults to 701 | # 'local.'. If you are bridging a broker to itself, it is important 702 | # that local_clientid and clientid do not match. 703 | #local_clientid 704 | 705 | # If set to true, publish notification messages to the local and remote brokers 706 | # giving information about the state of the bridge connection. Retained 707 | # messages are published to the topic $SYS/broker/connection//state 708 | # unless the notification_topic option is used. 709 | # If the message is 1 then the connection is active, or 0 if the connection has 710 | # failed. 711 | # This uses the last will and testament feature. 712 | #notifications true 713 | 714 | # Choose the topic on which notification messages for this bridge are 715 | # published. If not set, messages are published on the topic 716 | # $SYS/broker/connection//state 717 | #notification_topic 718 | 719 | # Set the client id to use on the remote end of this bridge connection. If not 720 | # defined, this defaults to 'name.hostname' where name is the connection name 721 | # and hostname is the hostname of this computer. 722 | # This replaces the old "clientid" option to avoid confusion. "clientid" 723 | # remains valid for the time being. 724 | #remote_clientid 725 | 726 | # Set the password to use when connecting to a broker that requires 727 | # authentication. This option is only used if remote_username is also set. 728 | # This replaces the old "password" option to avoid confusion. "password" 729 | # remains valid for the time being. 730 | #remote_password 731 | 732 | # Set the username to use when connecting to a broker that requires 733 | # authentication. 734 | # This replaces the old "username" option to avoid confusion. "username" 735 | # remains valid for the time being. 736 | #remote_username 737 | 738 | # Set the amount of time a bridge using the automatic start type will wait 739 | # until attempting to reconnect. 740 | # This option can be configured to use a constant delay time in seconds, or to 741 | # use a backoff mechanism based on "Decorrelated Jitter", which adds a degree 742 | # of randomness to when the restart occurs. 743 | # 744 | # Set a constant timeout of 20 seconds: 745 | # restart_timeout 20 746 | # 747 | # Set backoff with a base (start value) of 10 seconds and a cap (upper limit) of 748 | # 60 seconds: 749 | # restart_timeout 10 30 750 | # 751 | # Defaults to jitter with a base of 5 and cap of 30 752 | #restart_timeout 5 30 753 | 754 | # If the bridge has more than one address given in the address/addresses 755 | # configuration, the round_robin option defines the behaviour of the bridge on 756 | # a failure of the bridge connection. If round_robin is false, the default 757 | # value, then the first address is treated as the main bridge connection. If 758 | # the connection fails, the other secondary addresses will be attempted in 759 | # turn. Whilst connected to a secondary bridge, the bridge will periodically 760 | # attempt to reconnect to the main bridge until successful. 761 | # If round_robin is true, then all addresses are treated as equals. If a 762 | # connection fails, the next address will be tried and if successful will 763 | # remain connected until it fails 764 | #round_robin false 765 | 766 | # Set the start type of the bridge. This controls how the bridge starts and 767 | # can be one of three types: automatic, lazy and once. Note that RSMB provides 768 | # a fourth start type "manual" which isn't currently supported by mosquitto. 769 | # 770 | # "automatic" is the default start type and means that the bridge connection 771 | # will be started automatically when the broker starts and also restarted 772 | # after a short delay (30 seconds) if the connection fails. 773 | # 774 | # Bridges using the "lazy" start type will be started automatically when the 775 | # number of queued messages exceeds the number set with the "threshold" 776 | # parameter. It will be stopped automatically after the time set by the 777 | # "idle_timeout" parameter. Use this start type if you wish the connection to 778 | # only be active when it is needed. 779 | # 780 | # A bridge using the "once" start type will be started automatically when the 781 | # broker starts but will not be restarted if the connection fails. 782 | #start_type automatic 783 | 784 | # Set the number of messages that need to be queued for a bridge with lazy 785 | # start type to be restarted. Defaults to 10 messages. 786 | # Must be less than max_queued_messages. 787 | #threshold 10 788 | 789 | # If try_private is set to true, the bridge will attempt to indicate to the 790 | # remote broker that it is a bridge not an ordinary client. If successful, this 791 | # means that loop detection will be more effective and that retained messages 792 | # will be propagated correctly. Not all brokers support this feature so it may 793 | # be necessary to set try_private to false if your bridge does not connect 794 | # properly. 795 | #try_private true 796 | 797 | # Some MQTT brokers do not allow retained messages. MQTT v5 gives a mechanism 798 | # for brokers to tell clients that they do not support retained messages, but 799 | # this is not possible for MQTT v3.1.1 or v3.1. If you need to bridge to a 800 | # v3.1.1 or v3.1 broker that does not support retained messages, set the 801 | # bridge_outgoing_retain option to false. This will remove the retain bit on 802 | # all outgoing messages to that bridge, regardless of any other setting. 803 | #bridge_outgoing_retain true 804 | 805 | # If you wish to restrict the size of messages sent to a remote bridge, use the 806 | # bridge_max_packet_size option. This sets the maximum number of bytes for 807 | # the total message, including headers and payload. 808 | # Note that MQTT v5 brokers may provide their own maximum-packet-size property. 809 | # In this case, the smaller of the two limits will be used. 810 | # Set to 0 for "unlimited". 811 | #bridge_max_packet_size 0 812 | 813 | 814 | # ----------------------------------------------------------------- 815 | # Certificate based SSL/TLS support 816 | # ----------------------------------------------------------------- 817 | # Either bridge_cafile or bridge_capath must be defined to enable TLS support 818 | # for this bridge. 819 | # bridge_cafile defines the path to a file containing the 820 | # Certificate Authority certificates that have signed the remote broker 821 | # certificate. 822 | # bridge_capath defines a directory that will be searched for files containing 823 | # the CA certificates. For bridge_capath to work correctly, the certificate 824 | # files must have ".crt" as the file ending and you must run "openssl rehash 825 | # " each time you add/remove a certificate. 826 | #bridge_cafile 827 | #bridge_capath 828 | 829 | 830 | # If the remote broker has more than one protocol available on its port, e.g. 831 | # MQTT and WebSockets, then use bridge_alpn to configure which protocol is 832 | # requested. Note that WebSockets support for bridges is not yet available. 833 | #bridge_alpn 834 | 835 | # When using certificate based encryption, bridge_insecure disables 836 | # verification of the server hostname in the server certificate. This can be 837 | # useful when testing initial server configurations, but makes it possible for 838 | # a malicious third party to impersonate your server through DNS spoofing, for 839 | # example. Use this option in testing only. If you need to resort to using this 840 | # option in a production environment, your setup is at fault and there is no 841 | # point using encryption. 842 | #bridge_insecure false 843 | 844 | # Path to the PEM encoded client certificate, if required by the remote broker. 845 | #bridge_certfile 846 | 847 | # Path to the PEM encoded client private key, if required by the remote broker. 848 | #bridge_keyfile 849 | 850 | # ----------------------------------------------------------------- 851 | # PSK based SSL/TLS support 852 | # ----------------------------------------------------------------- 853 | # Pre-shared-key encryption provides an alternative to certificate based 854 | # encryption. A bridge can be configured to use PSK with the bridge_identity 855 | # and bridge_psk options. These are the client PSK identity, and pre-shared-key 856 | # in hexadecimal format with no "0x". Only one of certificate and PSK based 857 | # encryption can be used on one 858 | # bridge at once. 859 | #bridge_identity 860 | #bridge_psk 861 | 862 | 863 | # ================================================================= 864 | # External config files 865 | # ================================================================= 866 | 867 | # External configuration files may be included by using the 868 | # include_dir option. This defines a directory that will be searched 869 | # for config files. All files that end in '.conf' will be loaded as 870 | # a configuration file. It is best to have this as the last option 871 | # in the main file. This option will only be processed from the main 872 | # configuration file. The directory specified must not contain the 873 | # main configuration file. 874 | # Files within include_dir will be loaded sorted in case-sensitive 875 | # alphabetical order, with capital letters ordered first. If this option is 876 | # given multiple times, all of the files from the first instance will be 877 | # processed before the next instance. See the man page for examples. 878 | #include_dir 879 | --------------------------------------------------------------------------------