Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Jarno Rankinen | 4b3f7c6f45 |
|
@ -1,9 +1,9 @@
|
|||
name: Add issues to project
|
||||
|
||||
#on:
|
||||
# issues:
|
||||
# types:
|
||||
# - opened
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
name: Build release binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.22.1'
|
||||
- name: Build release binaries
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_TOKEN }}
|
|
@ -1,4 +1 @@
|
|||
.vscode/
|
||||
.idea/
|
||||
BUILD/*
|
||||
TMP/*
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
version: 1
|
||||
|
||||
before:
|
||||
hooks:
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
|
||||
archives:
|
||||
- format: binary
|
||||
name_template: >-
|
||||
{{ .ProjectName }}-
|
||||
{{- .Os }}-
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "^ci:"
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Jarno Rankinen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
171
README.md
171
README.md
|
@ -4,161 +4,22 @@ External control of an Enervent Pingvin
|
|||
Kotilämpö residential heating/ventilation
|
||||
unit via RS485 bus using the Modbus protocol.
|
||||
|
||||
Provides a REST API for integration into Home Assistant,
|
||||
with measurements and basic control over Pingvin functions.
|
||||
|
||||
Template YAML configurations for Home Assistant are included
|
||||
in the `homeassistant` folder, intended to be simple to copy-paste
|
||||
into Home Assistant's `configuration.yaml` with minimal necessary
|
||||
modifications. These include sensor configurations, helpers and automations for button functions
|
||||
and a ready made basic dashboard. No custom components are necessary.
|
||||
|
||||
![image](https://user-images.githubusercontent.com/50285623/228834067-503f9820-292c-4614-9316-6cec683e89ef.png)
|
||||
|
||||
The daemon is designed to run on a Linux host
|
||||
that has some sort of RS-485 connector attached.
|
||||
For development a RPi Zero W 1 with a
|
||||
connected [Zihatec RS 485 HAT](https://www.hwhardsoft.de/english/projects/rs485-shield/?mobile=1)
|
||||
has been used.
|
||||
|
||||
### Building
|
||||
- clone or download the repo
|
||||
- Build for the correct architecture, e.g. for Linux 32-bit ARM (Rpi Zero W 1):
|
||||
```
|
||||
cd /path/to/repo
|
||||
env GOOS=linux GOARCH=arm go build -o BUILD/enervent-ctrl-linux-arm32
|
||||
```
|
||||
|
||||
### Configuration:
|
||||
- CLI flags:
|
||||
```
|
||||
-cert string
|
||||
Path to SSL public key to use for HTTPS (default "~/.config/enervent-ctrl/certificate.pem")
|
||||
-debug
|
||||
Enable debug logging
|
||||
-disable-auth
|
||||
Disable HTTP basic authentication (default true)
|
||||
-enable-metrics
|
||||
Enable the built-in Prometheus exporter (default true)
|
||||
-httplog
|
||||
Enable HTTP access logging
|
||||
-interval int
|
||||
Set the interval of background updates (default 4)
|
||||
-key string
|
||||
Path to SSL private key to use for HTTPS (default "~/.config/enervent-ctrl/privatekey.pem")
|
||||
-logfile string
|
||||
Path to log file. Default is empty string, log to stdout
|
||||
-password string
|
||||
Password for HTTP Basic Authentication (default "enervent")
|
||||
-read-only
|
||||
Read only mode, no writes to device are allowed
|
||||
-regenerate-certs ~/.config/enervent-ctrl/server.crt
|
||||
Generate a new SSL certificate. A new one is generated on startup as ~/.config/enervent-ctrl/server.crt if it doesn't exist.
|
||||
-serial string
|
||||
Path to serial console for RS-485 connection. Defaults to /dev/ttyS0 (default "/dev/ttyS0")
|
||||
-username string
|
||||
Username for HTTP Basic Authentication (default "pingvin")
|
||||
```
|
||||
On first run, the daemon generates `~/.config/enervent-ctrl/configuration.yaml` with default values.
|
||||
Configuration options are the same as with CLI flags. CLI flags take precedence over the config file.
|
||||
- `serial_address:` Path to RS-485 serial device
|
||||
- `port:` TCP port for the REST API to listen on
|
||||
- `ssl_certificate:` Path to SSL certificate for HTTPS
|
||||
- `ssl_privatekey:` Path to SSL private key for HTTPS
|
||||
- `username:` Username for REST API HTTP Basic Auth
|
||||
- `password:` Password for REST API HTTP Basic Auth
|
||||
- `interval:` Interval of background updates from Modbus
|
||||
- `enable_metrics:` Enable the built-in Prometheus exporter
|
||||
- `log_file:` Path to log file, default logging is to STDOUT
|
||||
- `log_access:` Enable HTTP Access logging to logfile/STDOUT
|
||||
- `debug:` Enable debug logging
|
||||
|
||||
### Running
|
||||
- Upload the built executable along with `coils.csv` and `registers.csv` to the target host. The files should
|
||||
be placed in the same folder.
|
||||
- Run the binary as a regular user. Adding the user to the correct group for serial access may be necessary
|
||||
- To run persistently, you can use `screen`, `tmux`, or generate a user systemd service unit file.
|
||||
- Example systemd service file (named e.g. enervent-ctrl.service):
|
||||
```
|
||||
[Unit]
|
||||
Description=Enervent-ctrl
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
ExecStart=/path/to/enervent-ctrl-executable
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
- Replace paths in the file and place it under `~/.config/systemd/user`. Create the folder if it doesn't exist.
|
||||
- `systemctl --user daemon-reload`
|
||||
- `systemctl --user enable --now enervent-ctrl.service`
|
||||
- To let user services continue running after logging out:
|
||||
- `sudo loginctl enable-linger $USER`
|
||||
|
||||
***
|
||||
# Disclaimer:
|
||||
|
||||
**I am not responsible of possible damage to your device if you choose to follow these instructions**
|
||||
|
||||
**The manufacturer may void your warranty if you choose to follow these instructions**
|
||||
***
|
||||
|
||||
### Connecting to the Pingvin unit
|
||||
#### RPi/computer running the daemon
|
||||
- Connect an RS-485 adapter to the computer you intend to run the daemon on
|
||||
- Tested on:
|
||||
- RPi 4B and Zero W 1, generic x86_64 linux machines (Alma Linux 8 & 9, Fedora)
|
||||
- Zihatec RS-485 HAT with the Pis
|
||||
- generic USB-RS485 adapter (checksum errors considerably more often, but nothing critical)
|
||||
- Ensure the user you intend to run the daemon as has read/write privileges to the serial device.
|
||||
- **Not recommended and no need to run as root**
|
||||
- Usually adding the user running the executable to the `dialout` group gives permissions to serial devices
|
||||
|
||||
#### Pingvin
|
||||
- Shut down the main power of the unit
|
||||
- Disconnect the device from mains, discharge any static electricity before proceeding
|
||||
- A new motherboard seems to cost close to 1000€ + labour
|
||||
- Open the cover in which the power switch is attached to. No need to disconnect the switch, there
|
||||
should be enough length in the wires to move the lid with the switch connected out of the way
|
||||
![IMG_20230114_133625](https://user-images.githubusercontent.com/50285623/229897490-33d917be-9dea-4b74-bfed-c7b25f9f45f6.jpg)
|
||||
- Locate the green RS-485 connector on the motherboard, should be on the right edge
|
||||
- Schematics available from Enervent at [https://doc.enervent.com/op/op.ViewOnline.php?documentid=940&version=1](https://doc.enervent.com/op/op.ViewOnline.php?documentid=940&version=1), page 38 (finnish)
|
||||
![IMG_20230114_133824](https://user-images.githubusercontent.com/50285623/229898136-ce7dc020-6c33-4605-86ff-5285000cbbd2.jpg)
|
||||
- There should be available outlet holes to pass the wires through on the top of the electronics compartment.
|
||||
- The connector has a detachable plug part. Grab the top of the connector (the part with the screws) with plyers and carefully pull it out. This will make attaching the wire much easier
|
||||
- Attach wires by tightening the screws in the connector
|
||||
- Connect **A connector to A connector and B to B**. (they are not Tx/Rx like in many other serials)
|
||||
- **NOTE:** After reading quite a few forum posts, many RS-485 adapters seem to have printed the A and B the wrong way, I wouldn't be surprised if this was the case with Pingvin too.
|
||||
![IMG_20230114_133936](https://user-images.githubusercontent.com/50285623/229900176-5bac0027-80c6-4702-ab74-0ff2b9739507.jpg)
|
||||
- Plug the plug back to the Pingvin motherboard and close the cover and screws
|
||||
![IMG_20230114_135258](https://user-images.githubusercontent.com/50285623/229899975-45126a64-7344-4ca0-bfba-c4e524ebe2f8.jpg)
|
||||
- Reconnect mains and switch both devices on
|
||||
- Mixing A and B should be safe and won't break anything, but the daemon won't work. If that's the case, disconnect power again and switch the wires on the RPi end.
|
||||
|
||||
### Home Assistant
|
||||
|
||||
- There are so many variations for HASS configs, that definite instructions are hard to do.
|
||||
- All the YAMLs are intended to be copy-pasted to `configuration.yaml` (or files included to configuration.yaml)
|
||||
- Contents of `homeassistant/automations.yaml` to automations.yaml in your HA `config/` folder
|
||||
- Contents of `homeassistant/homeassistant-rest.yaml` and `homeassistant/helpers.yaml` to configuration.yaml in your HA `config/` folder
|
||||
- Replace IP_ADDRESS with the correct IP address, for example with sed: `sed -i 's/IP_ADDRESS/192.168.4.5/g' configuration.yaml`
|
||||
- If you set a different port for enervent-ctrl, use `sed -i 's/IP_ADDRESS:8888/192.168.4.5:9999/g' configuration.yaml`
|
||||
- Dashboard:
|
||||
- create an empty dashboard
|
||||
- opening the YAML editor in the HA Lovelace UI
|
||||
- copy the contents from `homeassistant/dashboard-en/fi.yaml` to the editor as-is.
|
||||
- Change the IP address, port, username and password according to your configuration
|
||||
- Restart Home Assistant (A full reload doesn't seem to be enough for all REST integration features to update)
|
||||
|
||||
|
||||
Work is part of my Bachelor's Thesis at Oulu University
|
||||
Work part of my Bachelor's Thesis at Oulu University
|
||||
of Applied Sciences.
|
||||
|
||||
Pingvin and Kotilämpö are registered trademarks of Enervent Zehnder Oy.
|
||||
The Python version under `enervent-ctrl-python`
|
||||
is an initial proof-of-concept,
|
||||
mainly to test that the hardware side of things
|
||||
works as expected. The main daemon is written
|
||||
in Go and the source is under `enervent-ctrl-go`
|
||||
|
||||
<sup><sub>Github is used to build the binaries and container images with Github Actions, and host pre-built releases.
|
||||
Mirrored from https://git.oranki.net/jarno/enervent-ctrl</sub></sup>
|
||||
The daemon is designed to run on a Linux host
|
||||
that has some sort of RS485 connector attached.
|
||||
For development a Raspberry Pi 4B was initially
|
||||
used for convenience, but after the Go
|
||||
implementation started, a RPi Zero W 1 with a
|
||||
connected [Zihatec RS 485 HAT](https://www.hwhardsoft.de/english/projects/rs485-shield/?mobile=1)
|
||||
has been used to make sure the daemon stays as
|
||||
lightweight as possible.
|
||||
|
||||
Pingvin and Kotilämpö are registered trademarks of Enervent Zehnder Oy.
|
||||
|
|
15
build.sh
15
build.sh
|
@ -1,15 +0,0 @@
|
|||
#!/bin/bash -x
|
||||
|
||||
pwd
|
||||
|
||||
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
echo -e "Usage: $0 [ARCH|-h|--help]"
|
||||
echo -e "\tARCH: amd64 (default), arm, arm64"
|
||||
exit
|
||||
fi
|
||||
|
||||
ARCH=${1:-"amd64"}
|
||||
|
||||
VERSION=$(grep -e 'version.*=' main.go | awk '{print $3}' | tr -d '"')
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH="$ARCH" go build -o "BUILD/enervent-ctrl-${VERSION}.linux-$ARCH" .
|
|
@ -0,0 +1,2 @@
|
|||
build.sh
|
||||
BUILD/*
|
|
@ -36,7 +36,7 @@
|
|||
35;-;-
|
||||
36;COIL_TEMP_DECREASE;Temperature decrease function desc
|
||||
37;COIL_OVERTIME;Programmatic equivalent of OVERTIME digital input.
|
||||
38;COIL_EMERG_STOP;Emergency stop switch type desc
|
||||
38;-;Emergency stop switch type desc
|
||||
39;-;-
|
||||
40;COIL_ECO_MODE;Eco mode desc
|
||||
41;COIL_ALARM_A;Alarm of class A active desc
|
|
|
@ -0,0 +1,17 @@
|
|||
module github.com/0ranki/enervent-ctrl/enervent-ctrl-go
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433
|
||||
github.com/goburrow/modbus v0.1.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/goburrow/serial v0.1.0 // indirect
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433 h1:QT2IRJnhIdCSr26LJktnZnBpHdiLfTrUFzLSdP3h9Wo=
|
||||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433/go.mod h1:r4Jb05+PuiVKHDYwSsSBuSz4LpOlC2DgOY4N58+K8Hk=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro=
|
||||
github.com/goburrow/modbus v0.1.0/go.mod h1:Kx552D5rLIS8E7TyUwQ/UdHEqvX5T8tyiGBTlzMcZBg=
|
||||
github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
|
||||
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,337 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/0ranki/enervent-ctrl/enervent-ctrl-go/pingvinKL"
|
||||
"github.com/0ranki/https-go"
|
||||
"github.com/gorilla/handlers"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Remember to dereference the symbolic links under ./static/html
|
||||
// prior to building the binary e.g. by using tar
|
||||
|
||||
//go:embed static/html/*
|
||||
var static embed.FS
|
||||
|
||||
var (
|
||||
version = "0.0.22"
|
||||
pingvin pingvinKL.PingvinKL
|
||||
config Conf
|
||||
usernamehash [32]byte
|
||||
passwordhash [32]byte
|
||||
)
|
||||
|
||||
type Conf struct {
|
||||
Port int `yaml:"port"`
|
||||
SslCertificate string `yaml:"ssl_certificate"`
|
||||
SslPrivatekey string `yaml:"ssl_privatekey"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Interval int `yaml:"interval"`
|
||||
LogAccess bool `yaml:"log_access"`
|
||||
Debug bool `yaml:"debug"`
|
||||
}
|
||||
|
||||
// HTTP Basic Authentication middleware for http.HandlerFunc
|
||||
func authHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
|
||||
// Based on https://www.alexedwards.net/blog/basic-authentication-in-go
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
userHash := sha256.Sum256([]byte(user))
|
||||
passHash := sha256.Sum256([]byte(pass))
|
||||
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
|
||||
if usernameMatch && passwordMatch {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(user) == 0 {
|
||||
user = "-"
|
||||
}
|
||||
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
// HTTP Basic Authentication middleware for http.Handler
|
||||
func authHandler(next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
userHash := sha256.Sum256([]byte(user))
|
||||
passHash := sha256.Sum256([]byte(pass))
|
||||
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
|
||||
if usernameMatch && passwordMatch {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(user) == 0 {
|
||||
user = "-"
|
||||
}
|
||||
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
// \/api/v1/coils endpoint
|
||||
func coils(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/")
|
||||
if len(pathparams[0]) == 0 {
|
||||
json.NewEncoder(w).Encode(pingvin.Coils)
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse coil address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
pingvin.ReadCoil(uint16(intaddr))
|
||||
json.NewEncoder(w).Encode(pingvin.Coils[intaddr])
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse coil address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
boolval, err := strconv.ParseBool(pathparams[1])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse coil value", pathparams[1])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
pingvin.WriteCoil(uint16(intaddr), boolval)
|
||||
json.NewEncoder(w).Encode(pingvin.Coils[intaddr])
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse coil address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
pingvin.WriteCoil(uint16(intaddr), !pingvin.Coils[intaddr].Value)
|
||||
json.NewEncoder(w).Encode(pingvin.Coils[intaddr])
|
||||
}
|
||||
}
|
||||
|
||||
// \/api/v1/registers endpoint
|
||||
func registers(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/registers/"), "/")
|
||||
if len(pathparams[0]) == 0 {
|
||||
json.NewEncoder(w).Encode(pingvin.Registers)
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse register address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
pingvin.ReadRegister(uint16(intaddr))
|
||||
json.NewEncoder(w).Encode(pingvin.Registers[intaddr])
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse register address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
intval, err := strconv.Atoi(pathparams[1])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse register value", pathparams[1])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
_, err = pingvin.WriteRegister(uint16(intaddr), uint16(intval))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
json.NewEncoder(w).Encode(pingvin.Registers[intaddr])
|
||||
}
|
||||
}
|
||||
|
||||
// \/status endpoint
|
||||
func status(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(pingvin.Status)
|
||||
}
|
||||
|
||||
// \/api/v1/temperature endpoint
|
||||
func temperature(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/temperature/"), "/")
|
||||
if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
|
||||
pingvin.Temperature(pathparams[0])
|
||||
json.NewEncoder(w).Encode(pingvin.Registers[135])
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
func serve(cert, key *string) {
|
||||
log.Println("Starting pingvinAPI...")
|
||||
http.HandleFunc("/api/v1/coils/", authHandlerFunc(coils))
|
||||
http.HandleFunc("/api/v1/status", authHandlerFunc(status))
|
||||
http.HandleFunc("/api/v1/registers/", authHandlerFunc(registers))
|
||||
http.HandleFunc("/api/v1/temperature/", authHandlerFunc(temperature))
|
||||
html, err := fs.Sub(static, "static/html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
htmlroot := http.FileServer(http.FS(html))
|
||||
http.HandleFunc("/", authHandler(htmlroot))
|
||||
logdst, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModeAppend)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if config.LogAccess {
|
||||
logdst = os.Stdout
|
||||
}
|
||||
handler := handlers.LoggingHandler(logdst, http.DefaultServeMux)
|
||||
err = http.ListenAndServeTLS(":8888", *cert, *key, handler)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate self-signed SSL keypair
|
||||
func generateCertificate(cert, key string) {
|
||||
opts := https.GenerateOptions{Host: "enervent-ctrl.local", RSABits: 4096, ValidFor: 10 * 365 * 24 * time.Hour}
|
||||
log.Println("Generating new self-signed SSL keypair")
|
||||
log.Println("This may take a while...")
|
||||
pub, priv, err := https.GenerateKeys(opts)
|
||||
if err != nil {
|
||||
log.Fatal("Error generating SSL certificate: ", err)
|
||||
}
|
||||
pingvin.Debug.Println("Certificate:\n", string(pub))
|
||||
pingvin.Debug.Println("Key:\n", string(priv))
|
||||
if err := os.WriteFile(key, priv, 0600); err != nil {
|
||||
log.Fatal("Error writing private key ", key, ": ", err)
|
||||
}
|
||||
log.Println("Wrote new SSL private key ", cert)
|
||||
if err := os.WriteFile(cert, pub, 0644); err != nil {
|
||||
log.Fatal("Error writing certificate ", cert, ": ", err)
|
||||
}
|
||||
log.Println("Wrote new SSL public key ", cert)
|
||||
}
|
||||
|
||||
// Read & parse the configuration file
|
||||
func parseConfigFile() {
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatal("Could not determine user home directory")
|
||||
}
|
||||
confpath := homedir + "/.config/enervent-ctrl"
|
||||
if _, err := os.Stat(confpath); err != nil {
|
||||
log.Println("Generating configuration directory", confpath)
|
||||
if err := os.MkdirAll(confpath, 0700); err != nil {
|
||||
log.Fatal("Failed to generate configuration directory:", err)
|
||||
}
|
||||
}
|
||||
conffile := confpath + "/configuration.yaml"
|
||||
yamldata, err := ioutil.ReadFile(conffile)
|
||||
if err != nil {
|
||||
log.Println("Configuration file", conffile, "not found")
|
||||
log.Println("Generating", conffile, "with default values")
|
||||
initDefaultConfig(confpath)
|
||||
if yamldata, err = ioutil.ReadFile(conffile); err != nil {
|
||||
log.Fatal("Error parsing configuration:", err)
|
||||
}
|
||||
}
|
||||
err = yaml.Unmarshal(yamldata, &config)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse YAML:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write the default configuration to $HOME/.config/enervent-ctrl/configuration.yaml
|
||||
func initDefaultConfig(confpath string) {
|
||||
config = Conf{
|
||||
8888,
|
||||
confpath + "/certificate.pem",
|
||||
confpath + "/privatekey.pem",
|
||||
"pingvin",
|
||||
"enervent",
|
||||
4,
|
||||
false,
|
||||
false,
|
||||
}
|
||||
conffile := confpath + "/configuration.yaml"
|
||||
confbytes, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
log.Println("Error writing default configuration:", err)
|
||||
}
|
||||
if err := os.WriteFile(conffile, confbytes, 0600); err != nil {
|
||||
log.Fatal("Failed to write default configuration:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Read configuration. CLI flags take presedence over configuration file
|
||||
func configure() {
|
||||
log.Println("Reading configuration")
|
||||
parseConfigFile()
|
||||
debugflag := flag.Bool("debug", config.Debug, "Enable debug logging")
|
||||
intervalflag := flag.Int("interval", config.Interval, "Set the interval of background updates")
|
||||
logaccflag := flag.Bool("httplog", config.LogAccess, "Enable HTTP access logging")
|
||||
generatecert := flag.Bool("regenerate-certs", false, "Generate a new SSL certificate. A new one is generated on startup as `~/.config/enervent-ctrl/server.crt` if it doesn't exist.")
|
||||
certflag := flag.String("cert", config.SslCertificate, "Path to SSL public key to use for HTTPS")
|
||||
keyflag := flag.String("key", config.SslPrivatekey, "Path to SSL private key to use for HTTPS")
|
||||
usernflag := flag.String("username", config.Username, "Username for HTTP Basic Authentication")
|
||||
passwflag := flag.String("password", config.Password, "Password for HTTP Basic Authentication")
|
||||
// TODO: log file flag
|
||||
flag.Parse()
|
||||
config.Debug = *debugflag
|
||||
config.Interval = *intervalflag
|
||||
config.LogAccess = *logaccflag
|
||||
config.SslCertificate = *certflag
|
||||
config.SslPrivatekey = *keyflag
|
||||
config.Username = *usernflag
|
||||
config.Password = *passwflag
|
||||
usernamehash = sha256.Sum256([]byte(config.Username))
|
||||
passwordhash = sha256.Sum256([]byte(config.Password))
|
||||
// Check that certificate file exists, generate if needed
|
||||
if _, err := os.Stat(config.SslCertificate); err != nil || *generatecert {
|
||||
generateCertificate(config.SslCertificate, config.SslPrivatekey)
|
||||
}
|
||||
// Enable debug if configured
|
||||
if config.Debug {
|
||||
log.Println("Debug logging enabled")
|
||||
}
|
||||
// Enable HTTP access logging if configured
|
||||
if config.LogAccess {
|
||||
log.Println("HTTP Access logging enabled")
|
||||
}
|
||||
log.Println("Update interval set to", config.Interval, "seconds")
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("enervent-ctrl version", version)
|
||||
configure()
|
||||
pingvin = pingvinKL.New(config.Debug)
|
||||
pingvin.Update()
|
||||
go pingvin.Monitor(config.Interval)
|
||||
serve(&config.SslCertificate, &config.SslPrivatekey)
|
||||
pingvin.Quit()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package pingvin
|
||||
package pingvinKL
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
@ -11,42 +11,39 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/goburrow/modbus"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// single coil data
|
||||
type pingvinCoil struct {
|
||||
Address int `json:"address"`
|
||||
Symbol string `json:"symbol"`
|
||||
Value bool `json:"value"`
|
||||
Description string `json:"description"`
|
||||
Reserved bool `json:"reserved"`
|
||||
PromDesc *prometheus.Desc `json:"-"`
|
||||
Address int `json:"address"`
|
||||
Symbol string `json:"symbol"`
|
||||
Value bool `json:"value"`
|
||||
Description string `json:"description"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
||||
// unit modbus data
|
||||
type Pingvin struct {
|
||||
Coils []*pingvinCoil
|
||||
Registers []*pingvinRegister
|
||||
Status *pingvinStatus
|
||||
buslock *sync.Mutex
|
||||
handler *modbus.RTUClientHandler
|
||||
modbusclient modbus.Client
|
||||
firstReadDone bool
|
||||
Debug PingvinLogger
|
||||
type PingvinKL struct {
|
||||
Coils []pingvinCoil
|
||||
Registers []pingvinRegister
|
||||
Status pingvinStatus
|
||||
buslock *sync.Mutex
|
||||
statuslock *sync.Mutex
|
||||
handler *modbus.RTUClientHandler
|
||||
modbusclient modbus.Client
|
||||
Debug PingvinLogger
|
||||
}
|
||||
|
||||
// single register data
|
||||
type pingvinRegister struct {
|
||||
Address int `json:"address"`
|
||||
Symbol string `json:"symbol"`
|
||||
Value int `json:"value"`
|
||||
Bitfield string `json:"bitfield"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Reserved bool `json:"reserved"`
|
||||
Multiplier int `json:"multiplier"`
|
||||
PromDesc *prometheus.Desc `json:"-"`
|
||||
Address int `json:"address"`
|
||||
Symbol string `json:"symbol"`
|
||||
Value int `json:"value"`
|
||||
Bitfield string `json:"bitfield"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Reserved bool `json:"reserved"`
|
||||
Multiplier int `json:"multiplier"`
|
||||
}
|
||||
|
||||
type pingvinMeasurements struct {
|
||||
|
@ -68,15 +65,13 @@ type pingvinStatus struct {
|
|||
HrcPct int `json:"hrc_pct"` // Heat recovery turn speed
|
||||
TempSetting float32 `json:"temp_setting"` // Requested room temperature
|
||||
FanPct int `json:"fan_pct"` // Circulation fan setting
|
||||
FanPctIn int `json:"fan_pct_in"` // Intake fan setting
|
||||
FanPctEx int `json:"fan_pct_ex"` // Exhaust fan setting
|
||||
Measurements pingvinMeasurements `json:"measurements"` // Measurements
|
||||
HrcEffIn int `json:"hrc_efficiency_in"` // Calculated HRC efficiency, intake
|
||||
HrcEffEx int `json:"hrc_efficiency_ex"` // Calculated HRC efficiency, extract
|
||||
OpMode string `json:"op_mode"` // Current operating mode, text representation
|
||||
Uptime string `json:"uptime"` // Unit uptime
|
||||
SystemTime string `json:"system_time"` // Time and date in unit
|
||||
Coils []*pingvinCoil `json:"coils"`
|
||||
Coils []pingvinCoil `json:"coils"`
|
||||
}
|
||||
|
||||
type PingvinLogger struct {
|
||||
|
@ -104,29 +99,17 @@ func (logger *PingvinLogger) Println(msg ...any) {
|
|||
}
|
||||
}
|
||||
|
||||
func newCoil(address string, symbol string, description string) *pingvinCoil {
|
||||
func newCoil(address string, symbol string, description string) pingvinCoil {
|
||||
addr, err := strconv.Atoi(address)
|
||||
if err != nil {
|
||||
log.Fatal("newCoil: Atoi: ", err)
|
||||
}
|
||||
reserved := symbol == "-" && description == "-"
|
||||
if !reserved {
|
||||
promdesc := strings.ToLower(symbol)
|
||||
zpadaddr := fmt.Sprintf("%02d", addr)
|
||||
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
|
||||
return &pingvinCoil{addr, symbol, false, description, reserved,
|
||||
prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", "pingvin", promdesc),
|
||||
description,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
||||
return &pingvinCoil{addr, symbol, false, description, reserved, nil}
|
||||
coil := pingvinCoil{addr, symbol, false, description, reserved}
|
||||
return coil
|
||||
}
|
||||
|
||||
func newRegister(address, symbol, typ, multiplier, description string) *pingvinRegister {
|
||||
func newRegister(address, symbol, typ, multiplier, description string) pingvinRegister {
|
||||
addr, err := strconv.Atoi(address)
|
||||
if err != nil {
|
||||
log.Fatal("newRegister: Atoi(address): ", err)
|
||||
|
@ -139,29 +122,8 @@ func newRegister(address, symbol, typ, multiplier, description string) *pingvinR
|
|||
}
|
||||
}
|
||||
reserved := symbol == "Reserved" && description == "Reserved"
|
||||
|
||||
if !reserved {
|
||||
promdesc := strings.ToLower(symbol)
|
||||
zpadaddr := fmt.Sprintf("%03d", addr)
|
||||
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
|
||||
return &pingvinRegister{
|
||||
addr,
|
||||
symbol,
|
||||
0,
|
||||
"0000000000000000",
|
||||
typ,
|
||||
description,
|
||||
reserved,
|
||||
multipl,
|
||||
prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", "pingvin", promdesc),
|
||||
description,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
||||
return &pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl, nil}
|
||||
register := pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl}
|
||||
return register
|
||||
}
|
||||
|
||||
// read a CSV file containing data for coils or registers
|
||||
|
@ -186,10 +148,9 @@ func readCsvLines(file string) [][]string {
|
|||
|
||||
// Create modbus.Handler, store it in p.handler,
|
||||
// connect the handler and create p.modbusclient (modbus.Client)
|
||||
func (p *Pingvin) createModbusClient(serial string) {
|
||||
// TODO: read configuration from file, mostly hardcoded for now
|
||||
log.Println("Connecting to serial console on", serial)
|
||||
p.handler = modbus.NewRTUClientHandler(serial)
|
||||
func (p *PingvinKL) createModbusClient() {
|
||||
// TODO: read configuration from file, hardcoded for now
|
||||
p.handler = modbus.NewRTUClientHandler("/dev/ttyS0")
|
||||
p.handler.BaudRate = 19200
|
||||
p.handler.DataBits = 8
|
||||
p.handler.Parity = "N"
|
||||
|
@ -204,7 +165,7 @@ func (p *Pingvin) createModbusClient(serial string) {
|
|||
p.modbusclient = modbus.NewClient(p.handler)
|
||||
}
|
||||
|
||||
func (p *Pingvin) Quit() {
|
||||
func (p *PingvinKL) Quit() {
|
||||
err := p.handler.Close()
|
||||
if err != nil {
|
||||
log.Println("ERROR: Quit:", err)
|
||||
|
@ -212,24 +173,12 @@ func (p *Pingvin) Quit() {
|
|||
}
|
||||
|
||||
// Update all coil values
|
||||
func (p *Pingvin) updateCoils() {
|
||||
var results []byte
|
||||
var err error
|
||||
for retries := 1; retries <= 5; retries++ {
|
||||
p.Debug.Println("Reading coils, attempt", retries)
|
||||
p.buslock.Lock()
|
||||
results, err = p.modbusclient.ReadCoils(0, uint16(len(p.Coils)))
|
||||
p.buslock.Unlock()
|
||||
if len(results) > 0 {
|
||||
break
|
||||
} else if retries == 4 {
|
||||
log.Println("ERROR: updateCoils: client.Readcoils: ", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("WARNING updateCoils: client.ReadCoils attempt %d: %s\n", retries, err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
func (p *PingvinKL) updateCoils() {
|
||||
p.buslock.Lock()
|
||||
results, err := p.modbusclient.ReadCoils(0, uint16(len(p.Coils)))
|
||||
p.buslock.Unlock()
|
||||
if err != nil {
|
||||
log.Fatal("updateCoils: client.ReadCoils: ", err)
|
||||
}
|
||||
// modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7,
|
||||
// second byte coils 8-15 etc.
|
||||
|
@ -252,13 +201,13 @@ func (p *Pingvin) updateCoils() {
|
|||
|
||||
// Read a single holding register, stores value in p.Registers
|
||||
// Returns integer value of register
|
||||
func (p *Pingvin) ReadRegister(addr uint16) (int, error) {
|
||||
func (p *PingvinKL) ReadRegister(addr uint16) (int, error) {
|
||||
p.buslock.Lock()
|
||||
results, err := p.modbusclient.ReadHoldingRegisters(addr, 1)
|
||||
p.buslock.Unlock()
|
||||
if err != nil {
|
||||
//log.Println("ERROR: ReadRegister:", err)
|
||||
return p.Registers[addr].Value, err
|
||||
log.Println("ERROR: ReadRegister:", err)
|
||||
return 0, err
|
||||
}
|
||||
if p.Registers[addr].Type == "uint16" {
|
||||
p.Registers[addr].Value = int(uint16(results[0]) << 8)
|
||||
|
@ -271,7 +220,7 @@ func (p *Pingvin) ReadRegister(addr uint16) (int, error) {
|
|||
}
|
||||
|
||||
// Update a single holding register
|
||||
func (p *Pingvin) WriteRegister(addr uint16, value uint16) (uint16, error) {
|
||||
func (p *PingvinKL) WriteRegister(addr uint16, value uint16) (uint16, error) {
|
||||
p.buslock.Lock()
|
||||
_, err := p.modbusclient.WriteSingleRegister(addr, value)
|
||||
p.buslock.Unlock()
|
||||
|
@ -285,14 +234,13 @@ func (p *Pingvin) WriteRegister(addr uint16, value uint16) (uint16, error) {
|
|||
return 0, err
|
||||
}
|
||||
if val == int(value) {
|
||||
log.Printf("Wrote register %d to value %d (%s: %s)", addr, p.Registers[addr].Value, p.Registers[addr].Symbol, p.Registers[addr].Description)
|
||||
return value, nil
|
||||
}
|
||||
return 0, fmt.Errorf("Failed to write register")
|
||||
}
|
||||
|
||||
// Update all holding register values
|
||||
func (p *Pingvin) updateRegisters() {
|
||||
func (p *PingvinKL) updateRegisters() {
|
||||
var err error
|
||||
regs := len(p.Registers)
|
||||
k := 0
|
||||
|
@ -304,8 +252,8 @@ func (p *Pingvin) updateRegisters() {
|
|||
if regs-k < 125 {
|
||||
r = regs - k
|
||||
}
|
||||
var results []byte
|
||||
for retries := 1; retries <= 5; retries++ {
|
||||
results := []byte{}
|
||||
for retries := 0; retries < 5; retries++ {
|
||||
p.Debug.Println("Reading registers, attempt", retries, "k:", k)
|
||||
p.buslock.Lock()
|
||||
results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r))
|
||||
|
@ -313,18 +261,11 @@ func (p *Pingvin) updateRegisters() {
|
|||
if len(results) > 0 {
|
||||
break
|
||||
} else if retries == 4 {
|
||||
log.Printf("ERROR: updateRegisters: max retries reached, giving up. client.ReadHoldingRegisters: %v", err)
|
||||
log.Printf("ERROR: error occurred when reading registers %d - %d", k, k+r-1)
|
||||
if !p.firstReadDone {
|
||||
panic("FATAL: Error on initial read")
|
||||
}
|
||||
return
|
||||
log.Fatal("updateRegisters: client.ReadHoldingRegisters: ", err)
|
||||
} else if err != nil {
|
||||
log.Printf("WARNING: updateRegisters: client.ReadHoldingRegisters attempt %d: %s", retries, err)
|
||||
log.Println("WARNING: updateRegisters: client.ReadHoldingRegisters: ", err)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
p.firstReadDone = true
|
||||
// The values represent 16 bit integers, but modbus works with bytes
|
||||
// Each even byte of the returned []byte is the 8 MSBs of a new 16-bit
|
||||
// value, so for each even byte in the reponse slice we bitshift the byte
|
||||
|
@ -366,37 +307,29 @@ func (p *Pingvin) updateRegisters() {
|
|||
|
||||
// Wrapper function for updating coils, registers and populating
|
||||
// p.Status for Home Assistant
|
||||
func (p *Pingvin) Update() {
|
||||
func (p *PingvinKL) Update() {
|
||||
p.updateCoils()
|
||||
p.updateRegisters()
|
||||
p.populateStatus()
|
||||
}
|
||||
|
||||
// Read single coil
|
||||
func (p *Pingvin) ReadCoil(n uint16) (err error) {
|
||||
var results []byte
|
||||
for retries := 1; retries <= 5; retries++ {
|
||||
p.buslock.Lock()
|
||||
results, err = p.modbusclient.ReadCoils(n, 1)
|
||||
p.buslock.Unlock()
|
||||
if len(results) > 0 && err == nil {
|
||||
break
|
||||
} else if retries == 4 {
|
||||
//log.Println("ERROR ReadCoil: client.ReadCoils: ", err)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Printf("WARNING: ReadCoil: client.ReadCoils attempt %d: %s", retries, err)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
func (p PingvinKL) ReadCoil(n uint16) ([]byte, error) {
|
||||
p.buslock.Lock()
|
||||
results, err := p.modbusclient.ReadCoils(n, 1)
|
||||
p.buslock.Unlock()
|
||||
if err != nil {
|
||||
log.Fatal("ReadCoil: client.ReadCoils: ", err)
|
||||
return nil, err
|
||||
}
|
||||
p.Coils[n].Value = results[0] == 1
|
||||
return
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Force a single coil
|
||||
func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
|
||||
func (p *PingvinKL) WriteCoil(n uint16, val bool) bool {
|
||||
if val {
|
||||
_ = p.checkMutexCoils(n) //, p.handler)
|
||||
p.checkMutexCoils(n, p.handler)
|
||||
}
|
||||
var value uint16 = 0
|
||||
if val {
|
||||
|
@ -408,20 +341,19 @@ func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
|
|||
if err != nil {
|
||||
log.Println("ERROR: WriteCoil: ", err)
|
||||
}
|
||||
if (val && results[0] != 255) && (!val && results[0] != 0) {
|
||||
if (val && results[0] == 255) || (!val && results[0] == 0) {
|
||||
log.Println("WriteCoil: wrote coil", n, "to value", val)
|
||||
} else {
|
||||
log.Println("ERROR: WriteCoil: failed to write coil")
|
||||
return false
|
||||
|
||||
}
|
||||
err = p.ReadCoil(n)
|
||||
if err != nil {
|
||||
log.Printf("ERROR WriteCoil: p.ReadCoil: %s", err)
|
||||
}
|
||||
log.Printf("Wrote coil %d to value %v (%s: %s)", n, p.Coils[n].Value, p.Coils[n].Symbol, p.Coils[n].Description)
|
||||
p.ReadCoil(n)
|
||||
return true
|
||||
}
|
||||
|
||||
// Force multiple coils
|
||||
func (p *Pingvin) WriteCoils(startaddr uint16, quantity uint16, vals []bool) error {
|
||||
func (p *PingvinKL) WriteCoils(startaddr uint16, quantity uint16, vals []bool) error {
|
||||
p.updateCoils()
|
||||
coilslice := p.Coils[startaddr:(startaddr + quantity)]
|
||||
if len(coilslice) != len(vals) {
|
||||
|
@ -472,7 +404,7 @@ func (p *Pingvin) WriteCoils(startaddr uint16, quantity uint16, vals []bool) err
|
|||
|
||||
// Some of the coils are mutually exclusive, and can only be 1 one at a time.
|
||||
// Check if coil is one of them and force all of them to 0 if so
|
||||
func (p *Pingvin) checkMutexCoils(addr uint16) error { //, handler *modbus.RTUClientHandler) error {
|
||||
func (p *PingvinKL) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandler) error {
|
||||
for _, mutexcoil := range mutexcoils {
|
||||
if mutexcoil == addr {
|
||||
for _, n := range mutexcoils {
|
||||
|
@ -493,8 +425,7 @@ func (p *Pingvin) checkMutexCoils(addr uint16) error { //, handler *modbus.RTUCl
|
|||
}
|
||||
|
||||
// populate p.Status struct for Home Assistant
|
||||
func (p *Pingvin) populateStatus() {
|
||||
p.Status = &pingvinStatus{}
|
||||
func (p *PingvinKL) populateStatus() {
|
||||
hpct := p.Registers[49].Value / p.Registers[49].Multiplier
|
||||
if hpct > 100 {
|
||||
p.Status.HeaterPct = hpct - 100
|
||||
|
@ -505,8 +436,6 @@ func (p *Pingvin) populateStatus() {
|
|||
}
|
||||
p.Status.TempSetting = float32(p.Registers[135].Value) / float32(p.Registers[135].Multiplier)
|
||||
p.Status.FanPct = p.Registers[774].Value / p.Registers[774].Multiplier
|
||||
p.Status.FanPctIn = p.Registers[3].Value / p.Registers[3].Multiplier
|
||||
p.Status.FanPctEx = p.Registers[4].Value / p.Registers[4].Multiplier
|
||||
p.Status.Measurements.Roomtemp1 = float32(p.Registers[1].Value) / float32(p.Registers[1].Multiplier)
|
||||
p.Status.Measurements.SupplyHeated = float32(p.Registers[8].Value) / float32(p.Registers[8].Multiplier)
|
||||
p.Status.Measurements.SupplyHrc = float32(p.Registers[7].Value) / float32(p.Registers[7].Multiplier)
|
||||
|
@ -562,7 +491,7 @@ func parseStatus(value int) string {
|
|||
// a decimal degree value (20.0 - 23.0), or full degrees (20-30)
|
||||
// Temperature must be between 20 and 30 deg Celsius, otherwise
|
||||
// returns an error
|
||||
func (p *Pingvin) Temperature(action string) error {
|
||||
func (p *PingvinKL) Temperature(action string) error {
|
||||
temperature := 0
|
||||
if action == "up" {
|
||||
temperature = p.Registers[135].Value + 1*p.Registers[135].Multiplier
|
||||
|
@ -600,7 +529,7 @@ func (p *Pingvin) Temperature(action string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Pingvin) Monitor(interval int) {
|
||||
func (p *PingvinKL) Monitor(interval int) {
|
||||
for {
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
p.Debug.Println("Updating values")
|
||||
|
@ -608,52 +537,12 @@ func (p *Pingvin) Monitor(interval int) {
|
|||
}
|
||||
}
|
||||
|
||||
// Implements prometheus.Describe()
|
||||
func (p *Pingvin) Describe(ch chan<- *prometheus.Desc) {
|
||||
for _, hreg := range p.Registers {
|
||||
if !hreg.Reserved {
|
||||
ch <- hreg.PromDesc
|
||||
}
|
||||
}
|
||||
for _, coil := range p.Coils {
|
||||
if !coil.Reserved {
|
||||
ch <- coil.PromDesc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implements prometheus.Collect()
|
||||
func (p *Pingvin) Collect(ch chan<- prometheus.Metric) {
|
||||
for _, hreg := range p.Registers {
|
||||
if !hreg.Reserved {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
hreg.PromDesc,
|
||||
prometheus.GaugeValue,
|
||||
float64(hreg.Value)/float64(hreg.Multiplier),
|
||||
)
|
||||
}
|
||||
}
|
||||
for _, coil := range p.Coils {
|
||||
val := 0
|
||||
if coil.Value {
|
||||
val = 1
|
||||
}
|
||||
if !coil.Reserved {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
coil.PromDesc,
|
||||
prometheus.GaugeValue,
|
||||
float64(val),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create a Pingvin struct, read coils and registers from CSVs
|
||||
func New(serial string, debug bool) *Pingvin {
|
||||
pingvin := Pingvin{}
|
||||
// create a PingvinKL struct, read coils and registers from CSVs
|
||||
func New(debug bool) PingvinKL {
|
||||
pingvin := PingvinKL{}
|
||||
pingvin.Debug.dbg = debug
|
||||
pingvin.buslock = &sync.Mutex{}
|
||||
pingvin.createModbusClient(serial)
|
||||
pingvin.createModbusClient()
|
||||
log.Println("Parsing coil data...")
|
||||
coilData := readCsvLines("coils.csv")
|
||||
for i := 0; i < len(coilData); i++ {
|
||||
|
@ -667,5 +556,5 @@ func New(serial string, debug bool) *Pingvin {
|
|||
newRegister(registerData[i][0], registerData[i][1], registerData[i][2], registerData[i][3], registerData[i][6]))
|
||||
}
|
||||
log.Println("Parsed", len(pingvin.Registers), "registers")
|
||||
return &pingvin
|
||||
return pingvin
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package pingvin
|
||||
package pingvinKL
|
||||
|
||||
import (
|
||||
"fmt"
|
|
@ -0,0 +1 @@
|
|||
../index.html
|
|
@ -0,0 +1 @@
|
|||
../index.html
|
|
@ -0,0 +1,6 @@
|
|||
*/__pycache__/
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
lib64
|
||||
share/
|
|
@ -0,0 +1 @@
|
|||
../index.html
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="/static/tabledata.css">
|
||||
<script src="/static/tabledata.js"></script>
|
||||
<meta charset="UTF-8">
|
||||
<title>Enervent Pingvin Kotilämpö</title>
|
||||
</head>
|
||||
<body onload="getData()">
|
||||
<table id="data">
|
||||
<caption>Coil values at <span id="time"></span></caption>
|
||||
<thead><th>Address</th><th>Value</th><th>Symbol</th><th>Description</th></thead>
|
||||
<tbody id="coildata"></tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
../index.html
|
|
@ -0,0 +1,17 @@
|
|||
.addr {
|
||||
text-align: center;
|
||||
}
|
||||
.val {
|
||||
text-align: center;
|
||||
}
|
||||
#data {
|
||||
padding: 2pt;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead {
|
||||
border-bottom: 1px solid;
|
||||
text-align: left;
|
||||
}
|
||||
td {
|
||||
padding: 2pt;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
function zeroPad(number) {
|
||||
return ("0" + number).slice(-2)
|
||||
}
|
||||
function getData() {
|
||||
now = new Date()
|
||||
Y = now.getFullYear()
|
||||
m = now.getMonth()
|
||||
d = now.getDate()
|
||||
H = zeroPad(now.getHours())
|
||||
M = zeroPad(now.getMinutes())
|
||||
S = zeroPad(now.getSeconds())
|
||||
document.getElementById('time').innerHTML = `${Y}-${m}-${d} ${H}:${M}:${S}`
|
||||
|
||||
error = false
|
||||
// The same index.html is used for both coil and register data,
|
||||
// change api url based on which we're looking at
|
||||
if (document.location.pathname == "/coils/") {
|
||||
url = "/api/v1/coils"
|
||||
}
|
||||
else if (document.location.pathname == "/registers/") {
|
||||
url = "/api/v1/registers"
|
||||
}
|
||||
else {
|
||||
document.getElementById("data").innerHTML = 'Page not found'
|
||||
error = true
|
||||
}
|
||||
if (!error) {
|
||||
// Fetch data from API
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching data: ${response.status}`)
|
||||
}
|
||||
return response.json()
|
||||
})
|
||||
.then((data) => {
|
||||
// Populate table
|
||||
document.getElementById('coildata').innerHTML = "";
|
||||
for (n=0; n<data.length; n++) {
|
||||
tablerow = `<tr><td class="addr" id="addr_${data[n].address}">${data[n].address}</td>\
|
||||
<td class ="val" id="value_${data[n].address}">${Number(data[n].value)}</td>\
|
||||
<td class="symbol" id="symbol_${data[n].address}">${data[n].symbol}</td>\
|
||||
<td class="desc" id="description_${data[n].address}">${data[n].description}</td></tr>`
|
||||
document.getElementById('coildata').innerHTML += tablerow
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Using setTimeout instead of setInterval to avoid possible connection issues
|
||||
// There's no need to update exactly every 5 seconds, the skew is fine
|
||||
setTimeout(getData, 1*1000);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
upstream enervent-ctrl {
|
||||
server localhost:8888;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
# SSL configuration
|
||||
#
|
||||
# listen 443 ssl default_server;
|
||||
# listen [::]:443 ssl default_server;
|
||||
#
|
||||
# Note: You should disable gzip for SSL traffic.
|
||||
# See: https://bugs.debian.org/773332
|
||||
#
|
||||
# Read up on ssl_ciphers to ensure a secure configuration.
|
||||
# See: https://bugs.debian.org/765782
|
||||
#
|
||||
# Self signed certs generated by the ssl-cert package
|
||||
# Don't use them in a production server!
|
||||
#
|
||||
# include snippets/snakeoil.conf;
|
||||
|
||||
root /home/jarno/enervent-ctrl/enervent-ctrl-python/html;
|
||||
index index.html;
|
||||
|
||||
server_name _;
|
||||
|
||||
location / {
|
||||
# First attempt to serve request as file, then
|
||||
# as directory, then fall back to displaying a 404.
|
||||
if ($http_user_agent ~* "^curl") {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_pass http://enervent-ctrl;
|
||||
}
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
#location ~ /static|/coils|/registers {
|
||||
# root /home/jarno/enervent-ctrl/enervent-ctrl-python/html;
|
||||
#}
|
||||
|
||||
location ~ /api {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_pass http://enervent-ctrl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.9.2
|
|
@ -0,0 +1,11 @@
|
|||
click==8.1.3
|
||||
Flask==2.2.2
|
||||
importlib-metadata==6.0.0
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.1
|
||||
minimalmodbus==2.0.1
|
||||
pyserial==3.5
|
||||
waitress==2.1.2
|
||||
Werkzeug==2.2.3
|
||||
zipp==3.11.0
|
|
@ -0,0 +1,188 @@
|
|||
import minimalmodbus
|
||||
import logging
|
||||
from flask import jsonify
|
||||
from threading import Lock
|
||||
from time import sleep
|
||||
|
||||
class PingvinCoil():
|
||||
"""Single coil data structure"""
|
||||
def __init__(self, symbol="-", description="-"):
|
||||
self.symbol = symbol
|
||||
self.value = False
|
||||
self.description = description
|
||||
self.reserved = symbol == "-" and description == "-"
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
"value": self.value,
|
||||
"symbol": self.symbol,
|
||||
"description": self.description,
|
||||
"reserved": self.reserved
|
||||
}
|
||||
|
||||
def get(self):
|
||||
return jsonify(self.serialize())
|
||||
|
||||
def flip(self):
|
||||
self.value = not self.value
|
||||
|
||||
class PingvinCoils():
|
||||
"""Class for handling Modbus coils"""
|
||||
## coil descriptions and symbols courtesy of Ensto Enervent
|
||||
## https://doc.enervent.com/out/out.ViewDocument.php?documentid=59
|
||||
coils = [
|
||||
PingvinCoil("COIL_STOP", "Stop"),
|
||||
PingvinCoil("COIL_AWAY", "Away mode"),
|
||||
PingvinCoil("COIL_AWAY_L", "Away Long mode"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil("COIL_MAX_H", "Max Heating"),
|
||||
PingvinCoil("COIL_MAX_C", "Max Cooling"),
|
||||
PingvinCoil("COIL_CO_BOOST_EN", "CO2 boost"),
|
||||
PingvinCoil("COIL_RH_BOOST_EN", "Relative humidity boost"),
|
||||
PingvinCoil("COIL_M_BOOST", "Manual boost 100%"),
|
||||
PingvinCoil("COIL_TEMP_BOOST_EN", "Temperature boost"),
|
||||
PingvinCoil("COIL_SNC", "Summer night cooling"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil("COIL_AWAY_H", "Heating enabled/disabled in AWAY mode"),
|
||||
PingvinCoil("COIL_AWAY_C", "Cooling enabled/disabled in AWAY mode"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil("COIL_LTO_ON", "Heat recycler state (running=1, stopped = 0)"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil("COIL_HEAT_ON", "After heater element state (On = 1, Off = 0)"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil("COIL_TEMP_DECREASE", "Temperature decrease function"),
|
||||
PingvinCoil("COIL_OVERTIME", "Programmatic equivalent of OVERTIME digital input"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil("COIL_ECO_MODE", "Eco mode"),
|
||||
PingvinCoil("COIL_ALARM_A", "Alarm of class A active"),
|
||||
PingvinCoil("COIL_ALARM_B", "Alarm of class B active"),
|
||||
PingvinCoil("COIL_CLK_PROG", "Clock program is currently active"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil("COIL_SILENT_MODE", "Silent mode"),
|
||||
PingvinCoil("COIL_STOP_SLP_COOLING", "Electrical heater cool-off function enabled when the machine has stopped"),
|
||||
PingvinCoil("COIL_SERVICE_EN", "Service reminder"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil("COIL_COOLING_EN", "Active cooling function enabled"),
|
||||
PingvinCoil("COIL_LTO_EN", "N/A"),
|
||||
PingvinCoil("COIL_HEATING_EN", "Active heating function enabled"),
|
||||
PingvinCoil("COIL_LTO_DEFROST_EN", "HRC defrosting function enabled during winter season"),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil(),
|
||||
PingvinCoil()
|
||||
]
|
||||
|
||||
def __init__(self, device, semaphore, debug=False):
|
||||
self.pingvin = device
|
||||
self.semaphore = semaphore
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.coils[item]
|
||||
|
||||
def update(self, debug=False):
|
||||
"""Fetch all coils values from device"""
|
||||
self.pingvin.serial.timeout = 0.2
|
||||
self.pingvin.debug = debug
|
||||
if debug: logging.info(f"{len(self.coils)} coils registered")
|
||||
self.semaphore.acquire()
|
||||
curvalues = self.pingvin.read_bits(0,len(self.coils),1)
|
||||
self.semaphore.release()
|
||||
for i, coil in enumerate(self.coils):
|
||||
self.coils[i].value = bool(curvalues[i])
|
||||
if debug: logging.info("Coil values read succesfully\n")
|
||||
|
||||
def fetchValue(self, address, debug=False):
|
||||
"""Update single coil value from device and return it"""
|
||||
self.pingvin.debug = debug
|
||||
if debug: logging.debug("Updating coil value from device to cache")
|
||||
self.semaphore.acquire()
|
||||
self.coils[address].value = bool(self.pingvin.read_bit(address, 1))
|
||||
self.semaphore.release()
|
||||
return self.value(address, debug)
|
||||
|
||||
def value(self, address, debug=False):
|
||||
"""Get single local coil value"""
|
||||
if debug: logging.debug("Reading coil value from cache")
|
||||
return self.coils[address].value
|
||||
|
||||
def print(self, debug=False):
|
||||
"""Human-readable print of all coil values"""
|
||||
coilvals = ""
|
||||
for i, coil in enumerate(self.coils):
|
||||
coilvals = coilvals + f"Coil {i : <{4}}{coil.value : <{2}} {coil.symbol : <{25}}{coil.description}\n"
|
||||
return coilvals
|
||||
|
||||
def serialize(self, include_reserved=False):
|
||||
"""Returns coil values as parseable Python object"""
|
||||
coilvals = []
|
||||
for i, coil in enumerate(self.coils):
|
||||
if not coil.reserved or include_reserved:
|
||||
coil = coil.serialize()
|
||||
coil['address'] = i
|
||||
coilvals.append(coil)
|
||||
return coilvals
|
||||
|
||||
def get(self, include_reserved=False, live=False, debug=False):
|
||||
"""Return all coil values in JSON format"""
|
||||
if live: self.update(debug)
|
||||
return jsonify(self.serialize(include_reserved))
|
||||
|
||||
def write(self, address):
|
||||
self.semaphore.acquire()
|
||||
self.pingvin.write_bit(address, int(not self.coils[address].value))
|
||||
if self.pingvin.read_bit(address, 1) != self.coils[address].value:
|
||||
self.coils[address].flip()
|
||||
self.semaphore.release()
|
||||
return True
|
||||
self.semaphore.release()
|
||||
return False
|
||||
|
||||
class PingvinKL():
|
||||
"""Class for communicating with an Enervent Pinvin Kotilämpö ventilation/heating unit"""
|
||||
def __init__(self, serialdevice='/dev/ttyS0', modbusaddr=1, debug=False):
|
||||
self.semaphore = Lock()
|
||||
self.pingvin = minimalmodbus.Instrument(serialdevice, modbusaddr)
|
||||
self.coils = PingvinCoils(self.pingvin, self.semaphore, debug)
|
||||
self.run = False
|
||||
|
||||
def monitor(self, interval=15, debug=False):
|
||||
if not self.run: # Prevent starting two monitor threads
|
||||
self.run = True
|
||||
logging.info("Starting data monitor loop")
|
||||
while self.run:
|
||||
if debug: logging.info("Data monitor updating coil data")
|
||||
self.coils.update(debug)
|
||||
sleep(interval)
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env python
|
||||
import logging
|
||||
from PingvinKL import PingvinKL
|
||||
from flask import Flask, request
|
||||
import threading
|
||||
from waitress import serve
|
||||
|
||||
VERSION = "0.0.1"
|
||||
DEBUG = False
|
||||
|
||||
## Logging configuration
|
||||
log = logging.getLogger(__name__)
|
||||
if DEBUG:
|
||||
dbglevel = logging.DEBUG
|
||||
else:
|
||||
dbglevel = logging.INFO
|
||||
logging.basicConfig(
|
||||
level=dbglevel,
|
||||
format='%(asctime)s %(message)s',
|
||||
datefmt='%y/%m/%d %H:%M:%S'
|
||||
)
|
||||
|
||||
pingvin = PingvinKL('/dev/ttyS0',1,debug=DEBUG)
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/api/v1/coils')
|
||||
def get_all():
|
||||
return pingvin.coils.get(include_reserved=request.args.get('include_reserved'),live=request.args.get('live'),debug=DEBUG)
|
||||
|
||||
@app.route('/api/v1/coils/<int:address>', methods=["GET","PUT"])
|
||||
def coil(address):
|
||||
if request.method == 'GET':
|
||||
coil = pingvin.coils[address].get()
|
||||
return coil
|
||||
elif request.method == 'PUT':
|
||||
return {"success": pingvin.coils.write(address)}
|
||||
|
||||
@app.route('/')
|
||||
def dump():
|
||||
return pingvin.coils.print(debug=DEBUG)
|
||||
|
||||
if __name__ == "__main__":
|
||||
log.info(f"Starting enervent-logger {VERSION}")
|
||||
datathread = threading.Thread(target=pingvin.monitor, kwargs={"interval": 2, "debug": DEBUG})
|
||||
datathread.start()
|
||||
# app.run(host='0.0.0.0', port=8888)
|
||||
serve(app, listen='*:8888', trusted_proxy='127.0.0.1', trusted_proxy_headers="x-forwarded-for x-forwarded-host x-forwarded-proto x-forwarded-port")
|
25
go.mod
25
go.mod
|
@ -1,25 +0,0 @@
|
|||
module github.com/0ranki/enervent-ctrl
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/0ranki/https-go v0.0.0-20230314073101-4eca22af948c
|
||||
github.com/goburrow/modbus v0.1.0
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/goburrow/serial v0.1.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/prometheus/client_model v0.6.0 // indirect
|
||||
github.com/prometheus/common v0.50.0 // indirect
|
||||
github.com/prometheus/procfs v0.13.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
520
go.sum
520
go.sum
|
@ -1,520 +0,0 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/0ranki/https-go v0.0.0-20230314073101-4eca22af948c h1:Tmui5U+C7KF4gYHnpXxe2sfROcrGksSmFheTVJAHdLo=
|
||||
github.com/0ranki/https-go v0.0.0-20230314073101-4eca22af948c/go.mod h1:r4Jb05+PuiVKHDYwSsSBuSz4LpOlC2DgOY4N58+K8Hk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro=
|
||||
github.com/goburrow/modbus v0.1.0/go.mod h1:Kx552D5rLIS8E7TyUwQ/UdHEqvX5T8tyiGBTlzMcZBg=
|
||||
github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
|
||||
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
||||
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
|
||||
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
185
handlers.go
185
handlers.go
|
@ -1,185 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTTP Basic Authentication middleware for http.HandlerFunc
|
||||
// This is used for the API
|
||||
func authHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
|
||||
// Based on https://www.alexedwards.net/blog/basic-authentication-in-go
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.DisableAuth {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
userHash := sha256.Sum256([]byte(user))
|
||||
passHash := sha256.Sum256([]byte(pass))
|
||||
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
|
||||
if usernameMatch && passwordMatch {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(user) == 0 {
|
||||
user = "-"
|
||||
}
|
||||
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
// HTTP Basic Authentication middleware for http.Handler
|
||||
// Used for the HTML monitor views
|
||||
func authHandler(next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.DisableAuth {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
userHash := sha256.Sum256([]byte(user))
|
||||
passHash := sha256.Sum256([]byte(pass))
|
||||
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
|
||||
if usernameMatch && passwordMatch {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(user) == 0 {
|
||||
user = "-"
|
||||
}
|
||||
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
// /api/v1/coils endpoint
|
||||
func coils(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/")
|
||||
if len(pathparams[0]) == 0 {
|
||||
_ = json.NewEncoder(w).Encode(device.Coils)
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse coil address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = device.ReadCoil(uint16(intaddr))
|
||||
if err != nil {
|
||||
log.Println("ERROR ReadCoil: client.ReadCoils: ", err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse coil address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
boolval, err := strconv.ParseBool(pathparams[1])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse coil value", pathparams[1])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if config.ReadOnly {
|
||||
log.Println("WARNING: Read only mode, refusing to write to device")
|
||||
} else {
|
||||
device.WriteCoil(uint16(intaddr), boolval)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse coil address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if config.ReadOnly {
|
||||
log.Println("WARNING: Read only mode, refusing to write to device")
|
||||
} else {
|
||||
device.WriteCoil(uint16(intaddr), !device.Coils[intaddr].Value)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
|
||||
}
|
||||
}
|
||||
|
||||
// /api/v1/registers endpoint
|
||||
func registers(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/registers/"), "/")
|
||||
if len(pathparams[0]) == 0 {
|
||||
_ = json.NewEncoder(w).Encode(device.Registers)
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse register address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
_, err = device.ReadRegister(uint16(intaddr))
|
||||
if err != nil {
|
||||
log.Println("ERROR: ReadRegister:", err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(device.Registers[intaddr])
|
||||
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
|
||||
intaddr, err := strconv.Atoi(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse register address", pathparams[0])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
intval, err := strconv.Atoi(pathparams[1])
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not parse register value", pathparams[1])
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if config.ReadOnly {
|
||||
log.Println("WARNING: Read only mode, refusing to write to device")
|
||||
} else {
|
||||
_, err = device.WriteRegister(uint16(intaddr), uint16(intval))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(device.Registers[intaddr])
|
||||
}
|
||||
}
|
||||
|
||||
// /status endpoint
|
||||
func status(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(device.Status)
|
||||
}
|
||||
|
||||
// /api/v1/temperature endpoint
|
||||
func temperature(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/temperature/"), "/")
|
||||
if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
|
||||
err := device.Temperature(pathparams[0])
|
||||
if err != nil {
|
||||
log.Println("ERROR: ", err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(device.Registers[135])
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,323 +1,103 @@
|
|||
## FAN / HEATER CONTROL AUTOMATIONS
|
||||
## THESE WILL AFFECT THE BEHAVIOUR OF THE UNIT
|
||||
- alias: Penguin Auto Heater Disable v0.2.0
|
||||
description: |-
|
||||
Temperature is 0.5 over setpoint and outside temperature over 10
|
||||
-> heater off
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- sensor.penguin_temperature_delta
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 0
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.penguin_temperature_delta
|
||||
above: 0.5
|
||||
condition:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_intake_air
|
||||
above: 10
|
||||
value_template: " {{ states['sensor.penguin_intake_air'].state }}"
|
||||
- condition: state
|
||||
entity_id: sun.sun
|
||||
state: above_horizon
|
||||
action:
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 15
|
||||
seconds: 0
|
||||
milliseconds: 0
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_intake_air
|
||||
above: 10
|
||||
value_template: "{{ states['sensor.penguin_intake_air'].state }}"
|
||||
- service: rest_command.penguin_heater_disable
|
||||
data: { }
|
||||
- service: input_boolean.turn_off
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_after_heater
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 5
|
||||
milliseconds: 0
|
||||
- service: rest_command.penguin_circulation_adaptive
|
||||
data: { }
|
||||
mode: single
|
||||
- alias: Penguin Auto Heater Enable v0.2.0
|
||||
description: |-
|
||||
Temperature below setpoint
|
||||
-> heater on
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.penguin_temperature_delta
|
||||
value_template: "{{ states['sensor.penguin_temperature_delta'].state }}"
|
||||
below: 0
|
||||
- platform: sun
|
||||
event: sunset
|
||||
offset: 0
|
||||
enabled: true
|
||||
condition: [ ]
|
||||
action:
|
||||
- service: rest_command.penguin_heater_enable
|
||||
data: { }
|
||||
- service: input_boolean.turn_on
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_after_heater
|
||||
mode: single
|
||||
- alias: Penguin Auto Heating increase v0.2.0
|
||||
description: |-
|
||||
Temperature 0.2 below setpoint
|
||||
-> circulation fan to manual
|
||||
-> max heating on
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.penguin_temperature_delta
|
||||
below: -0.2
|
||||
value_template: "{{ states['sensor.penguin_temperature_delta'].state }}"
|
||||
condition: [ ]
|
||||
action:
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 15
|
||||
seconds: 0
|
||||
milliseconds: 0
|
||||
enabled: true
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_temperature_delta
|
||||
value_template: "{{ states['sensor.penguin_temperature_delta'].state }}"
|
||||
below: -0.2
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_fan_control
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_max_heating_on
|
||||
data: { }
|
||||
- service: rest_command.penguin_circulation_manual
|
||||
data: { }
|
||||
enabled: false
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 5
|
||||
milliseconds: 0
|
||||
- service: rest_command.penguin_max_heating_on
|
||||
data: { }
|
||||
mode: single
|
||||
- alias: Penguin Auto Heating decrease v0.2.0
|
||||
description: |-
|
||||
Temperature 0.2 over setpoint
|
||||
-> adaptive circulation on (fan should go to minimum allowed)
|
||||
This works sometimes, sometimes doesn't, you may want to disable this
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.penguin_temperature_delta
|
||||
value_template: "{{ states['sensor.penguin_temperature_delta'].state }}"
|
||||
above: 0.2
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_fan_control
|
||||
state: "on"
|
||||
action:
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 15
|
||||
seconds: 0
|
||||
milliseconds: 0
|
||||
enabled: true
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_temperature_delta
|
||||
above: 0.2
|
||||
value_template: "{{ states['sensor.penguin_temperature_delta'].state }}"
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_fan_control
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_circulation_adaptive
|
||||
data: { }
|
||||
mode: single
|
||||
- alias: Penguin Auto Max Heating v0.2.0
|
||||
description: |-
|
||||
Temperature 0.3 below setpoint
|
||||
-> Max heat and max circulation (adaptive circulation on, with max heating means max fan setting)
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.penguin_temperature_delta
|
||||
value_template: "{{ states['sensor.penguin_temperature_delta'].state }}"
|
||||
below: -0.3
|
||||
condition: [ ]
|
||||
action:
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 15
|
||||
seconds: 0
|
||||
milliseconds: 0
|
||||
enabled: true
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_temperature_delta
|
||||
below: -0.3
|
||||
value_template: "{{ states['sensor.penguin_temperature_delta'].state }}"
|
||||
enabled: true
|
||||
- service: rest_command.penguin_max_heating_off
|
||||
data: { }
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 5
|
||||
milliseconds: 0
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_fan_control
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_circulation_adaptive
|
||||
data: { }
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 5
|
||||
milliseconds: 0
|
||||
- service: rest_command.penguin_max_heating_on
|
||||
data: { }
|
||||
mode: single
|
||||
automation:
|
||||
## Max heating
|
||||
- alias: Penguin Max Heating input
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_max_heating
|
||||
condition: []
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
state: "on"
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_max_heating
|
||||
state: "off"
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_room_temperature_1
|
||||
below: input_number.penguin_temperature_setting_helper
|
||||
then:
|
||||
- service: rest_command.penguin_max_heating_on
|
||||
data: {}
|
||||
else: []
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
state: "off"
|
||||
then:
|
||||
- service: rest_command.penguin_max_heating_off
|
||||
data: {}
|
||||
- if:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_room_temperature_1
|
||||
above: input_number.penguin_temperature_setting_helper
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_off
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
mode: single
|
||||
- alias: Penguin Max Heating sensor
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- binary_sensor.penguin_max_heating
|
||||
condition: []
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_max_heating
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_on
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
else:
|
||||
- service: input_boolean.turn_off
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
mode: single
|
||||
|
||||
## DASHBOARD TOGGLE/SETTING RELATED AUTOMATIONS
|
||||
## REQUIRED FOR e.g. SETTING THE TEMPERATURE SETPOINT VIA HA AND OTHER ACTIONS
|
||||
- alias: Penguin After Heater Input v0.2.0
|
||||
description: "Actions to take when input_boolean.penguin_after_heater is toggled"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_after_heater
|
||||
condition: [ ]
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_after_heater
|
||||
state: "on"
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_after_heater_enabled
|
||||
state: "off"
|
||||
then:
|
||||
- service: rest_command.penguin_heater_enable
|
||||
data: { }
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_after_heater
|
||||
state: "off"
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_after_heater_enabled
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_heater_disable
|
||||
data: { }
|
||||
mode: single
|
||||
- alias: Penguin boost input v0.2.0
|
||||
description: "Actions when toggling input_boolean.penguin_boost"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_boost
|
||||
condition: [ ]
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_boost
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_boost_on
|
||||
data: { }
|
||||
else:
|
||||
- service: rest_command.penguin_boost_off
|
||||
data: { }
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 5
|
||||
milliseconds: 0
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_boost
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_on
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_boost
|
||||
else:
|
||||
- service: input_boolean.turn_off
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_boost
|
||||
mode: single
|
||||
- alias: Penguin circulation fan mode sensor v0.2.0
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- binary_sensor.penguin_circulation_adaptive
|
||||
condition: [ ]
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_circulation_adaptive
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_on
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||
else:
|
||||
- service: input_boolean.turn_off
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 2
|
||||
milliseconds: 0
|
||||
- service: homeassistant.update_entity
|
||||
data: { }
|
||||
target:
|
||||
entity_id: sensor.penguin_circulation_fan_pct
|
||||
- service: homeassistant.update_entity
|
||||
data: { }
|
||||
target:
|
||||
entity_id: sensor.penguin_operating_mode
|
||||
mode: single
|
||||
- alias: Penguin circulation fan mode v0.2.0
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_circulation_fan_adaptive
|
||||
condition: [ ]
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_circulation_adaptive
|
||||
data: { }
|
||||
else:
|
||||
- service: rest_command.penguin_circulation_manual
|
||||
data: { }
|
||||
mode: single
|
||||
- alias: Penguin max cooling input v0.2.0
|
||||
|
||||
## Max cooling
|
||||
- alias: Penguin max cooling sensor
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- binary_sensor.penguin_max_cooling
|
||||
condition: []
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_max_cooling
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_on
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_cooling
|
||||
else:
|
||||
- service: input_boolean.turn_off
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_cooling
|
||||
mode: single
|
||||
- alias: Penguin max cooling input
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_max_cooling
|
||||
condition: [ ]
|
||||
condition: []
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
|
@ -331,15 +111,15 @@
|
|||
above: input_number.penguin_temperature_setting_helper
|
||||
then:
|
||||
- service: rest_command.penguin_max_cooling_on
|
||||
data: { }
|
||||
else: [ ]
|
||||
data: {}
|
||||
else: []
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_max_cooling
|
||||
state: "off"
|
||||
then:
|
||||
- service: rest_command.penguin_max_cooling_off
|
||||
data: { }
|
||||
data: {}
|
||||
- if:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_room_temperature_1
|
||||
|
@ -349,224 +129,111 @@
|
|||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_off
|
||||
data: { }
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_cooling
|
||||
mode: single
|
||||
- alias: Penguin max cooling sensor v0.2.0
|
||||
|
||||
|
||||
## Circulation fan mode
|
||||
- alias: Penguin circulation fan mode
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_circulation_fan_adaptive
|
||||
condition: []
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_circulation_adaptive
|
||||
data: {}
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||
state: "off"
|
||||
then:
|
||||
- service: rest_command.penguin_circulation_manual
|
||||
data: {}
|
||||
mode: single
|
||||
- alias: Penguin circulation fan mode sensor
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- binary_sensor.penguin_max_cooling
|
||||
condition: [ ]
|
||||
- binary_sensor.penguin_circulation_adaptive
|
||||
condition: []
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_max_cooling
|
||||
entity_id: binary_sensor.penguin_circulation_adaptive
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_on
|
||||
data: { }
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_cooling
|
||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||
else:
|
||||
- service: input_boolean.turn_off
|
||||
data: { }
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_cooling
|
||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||
mode: single
|
||||
- alias: Penguin Max Heating input v0.2.0
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_max_heating
|
||||
condition: [ ]
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
state: "on"
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_max_heating
|
||||
state: "off"
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_room_temperature_1
|
||||
below: input_number.penguin_temperature_setting_helper
|
||||
then:
|
||||
- service: rest_command.penguin_max_heating_on
|
||||
data: { }
|
||||
else: [ ]
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
state: "off"
|
||||
then:
|
||||
- service: rest_command.penguin_max_heating_off
|
||||
data: { }
|
||||
- if:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.penguin_room_temperature_1
|
||||
above: input_number.penguin_temperature_setting_helper
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_off
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
mode: single
|
||||
- alias: Penguin Max Heating sensor v0.2.0
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- binary_sensor.penguin_max_heating
|
||||
condition: [ ]
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_max_heating
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_on
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
else:
|
||||
- service: input_boolean.turn_off
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_max_heating
|
||||
mode: single
|
||||
- alias: Penguin overpressure input v0.2.0
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_overpressure
|
||||
condition: [ ]
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_overpressure
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_overpressure_on
|
||||
data: { }
|
||||
else:
|
||||
- service: rest_command.penguin_overpressure_off
|
||||
data: { }
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 5
|
||||
milliseconds: 0
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.penguin_overpressure
|
||||
state: "on"
|
||||
then:
|
||||
- service: input_boolean.turn_on
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_overpressure
|
||||
else:
|
||||
- service: input_boolean.turn_off
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_boolean.penguin_overpressure
|
||||
mode: single
|
||||
- alias: Penguin SNC input v0.2.0
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_boolean.penguin_snc
|
||||
condition: [ ]
|
||||
action:
|
||||
- if:
|
||||
- condition: state
|
||||
entity_id: input_boolean.penguin_snc
|
||||
state: "on"
|
||||
then:
|
||||
- service: rest_command.penguin_snc_enable
|
||||
data: { }
|
||||
else:
|
||||
- service: rest_command.penguin_snc_disable
|
||||
data: { }
|
||||
mode: single
|
||||
- alias: Penguin temperature down v0.2.0
|
||||
|
||||
|
||||
## Target temperature setting automations
|
||||
- alias: Penguin temperature down
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_button.penguin_temperature_down
|
||||
condition: [ ]
|
||||
condition: []
|
||||
action:
|
||||
- service: rest_command.penguin_temperature_down
|
||||
data: {}
|
||||
- service: input_number.decrement
|
||||
data: { }
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_number.penguin_temperature_setting_helper
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 3
|
||||
milliseconds: 0
|
||||
- service: rest_command.penguin_temperature_set
|
||||
data: { }
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 1
|
||||
milliseconds: 0
|
||||
- service: homeassistant.update_entity
|
||||
data: { }
|
||||
data: {}
|
||||
target:
|
||||
entity_id: sensor.penguin_temperature_setting
|
||||
mode: restart
|
||||
- alias: Penguin temperature up v0.2.0
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_button.penguin_temperature_up
|
||||
condition: [ ]
|
||||
action:
|
||||
- service: input_number.increment
|
||||
data: { }
|
||||
target:
|
||||
entity_id: input_number.penguin_temperature_setting_helper
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 3
|
||||
milliseconds: 0
|
||||
- service: rest_command.penguin_temperature_set
|
||||
data: { }
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 1
|
||||
milliseconds: 0
|
||||
- service: homeassistant.update_entity
|
||||
data: { }
|
||||
target:
|
||||
entity_id: sensor.penguin_temperature_setting
|
||||
mode: restart
|
||||
- alias: Penguin Temperature setting updater v0.2.0
|
||||
mode: single
|
||||
- alias: Penguin Temperature setting updater
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- sensor.penguin_temperature_setting
|
||||
condition: [ ]
|
||||
condition: []
|
||||
action:
|
||||
- service: input_number.set_value
|
||||
data:
|
||||
value: "{{ states('sensor.penguin_temperature_setting') }}"
|
||||
target:
|
||||
entity_id: input_number.penguin_temperature_setting_helper
|
||||
mode: single
|
||||
- alias: Penguin temperature up
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- input_button.penguin_temperature_up
|
||||
condition: []
|
||||
action:
|
||||
- service: rest_command.penguin_temperature_up
|
||||
data: {}
|
||||
- service: input_number.increment
|
||||
data: {}
|
||||
target:
|
||||
entity_id: input_number.penguin_temperature_setting_helper
|
||||
- service: homeassistant.update_entity
|
||||
data: {}
|
||||
target:
|
||||
entity_id: sensor.penguin_temperature_setting
|
||||
mode: single
|
|
@ -1,6 +1,5 @@
|
|||
views:
|
||||
- title: Pingvin
|
||||
icon: mdi:penguin
|
||||
- title: Penguin
|
||||
cards:
|
||||
- type: vertical-stack
|
||||
cards:
|
||||
|
@ -20,22 +19,11 @@ views:
|
|||
green: 0
|
||||
yellow: 0
|
||||
red: 100
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: input_boolean.penguin_after_heater
|
||||
name: Heating allowed
|
||||
secondary_info: last-changed
|
||||
- entity: input_boolean.penguin_snc
|
||||
icon: mdi:snowflake-thermometer
|
||||
name: Summer Night Cooling
|
||||
- entity: input_boolean.penguin_fan_control
|
||||
name: Circulation Fan Control
|
||||
state_color: true
|
||||
- square: false
|
||||
columns: 4
|
||||
columns: 3
|
||||
type: grid
|
||||
cards:
|
||||
- show_name: false
|
||||
- show_name: true
|
||||
show_icon: true
|
||||
type: button
|
||||
tap_action:
|
||||
|
@ -43,111 +31,49 @@ views:
|
|||
entity: input_boolean.penguin_circulation_fan_adaptive
|
||||
name: Adaptive circulation
|
||||
show_state: false
|
||||
- type: conditional
|
||||
conditions:
|
||||
- entity: binary_sensor.penguin_max_heating_enabled
|
||||
state: 'on'
|
||||
card:
|
||||
show_name: false
|
||||
show_icon: true
|
||||
type: button
|
||||
tap_action:
|
||||
action: toggle
|
||||
entity: input_boolean.penguin_max_heating
|
||||
name: Max heating
|
||||
show_state: false
|
||||
- type: conditional
|
||||
conditions:
|
||||
- entity: binary_sensor.penguin_max_heating_enabled
|
||||
state: 'off'
|
||||
card:
|
||||
show_name: false
|
||||
show_icon: true
|
||||
type: button
|
||||
tap_action:
|
||||
action: toggle
|
||||
entity: input_boolean.penguin_max_cooling
|
||||
name: Max cooling
|
||||
show_state: false
|
||||
- show_name: false
|
||||
- show_name: true
|
||||
show_icon: true
|
||||
type: button
|
||||
tap_action:
|
||||
action: toggle
|
||||
icon: mdi:fireplace
|
||||
entity: input_boolean.penguin_overpressure
|
||||
- show_name: false
|
||||
show_icon: true
|
||||
type: button
|
||||
tap_action:
|
||||
action: toggle
|
||||
icon: mdi:fan-plus
|
||||
entity: input_boolean.penguin_boost
|
||||
- square: false
|
||||
columns: 3
|
||||
type: grid
|
||||
cards:
|
||||
- show_name: false
|
||||
show_icon: true
|
||||
type: button
|
||||
tap_action:
|
||||
action: toggle
|
||||
entity: input_button.penguin_temperature_down
|
||||
icon: mdi:minus
|
||||
- type: gauge
|
||||
entity: input_number.penguin_temperature_setting_helper
|
||||
name: ' '
|
||||
needle: true
|
||||
min: 16
|
||||
max: 30
|
||||
- show_name: false
|
||||
show_icon: true
|
||||
type: button
|
||||
tap_action:
|
||||
action: toggle
|
||||
entity: input_button.penguin_temperature_up
|
||||
icon: mdi:plus
|
||||
entity: input_boolean.penguin_max_heating
|
||||
name: Max heating
|
||||
show_state: false
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: sensor.penguin_operating_mode
|
||||
name: Tila
|
||||
secondary_info: last-changed
|
||||
name: Mode
|
||||
- entity: sensor.penguin_room_temperature_1
|
||||
name: Room temperature
|
||||
- entity: sensor.penguin_temperature_setting
|
||||
name: Temperature setpoint
|
||||
- entity: sensor.penguin_circulation_fan_pct
|
||||
name: Circulation fan pct
|
||||
secondary_info: last-changed
|
||||
name: Criculation fan
|
||||
state_color: false
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: sensor.penguin_intake_air
|
||||
name: Outside air at unit
|
||||
name: Outside air at machine
|
||||
- entity: sensor.penguin_intake_air_24h
|
||||
name: Outside air 24h avg
|
||||
- entity: sensor.penguin_supply_air_hrc
|
||||
name: Intake after HRC
|
||||
name: Supply air after HRC
|
||||
- entity: sensor.penguin_supply_air_humidity
|
||||
name: Supply air humidity
|
||||
- entity: sensor.penguin_supply_air
|
||||
name: Supply air
|
||||
- entity: sensor.penguin_return_water
|
||||
name: Return water temperature
|
||||
name: Return water
|
||||
- entity: sensor.penguin_extract_air
|
||||
name: Extract before HRC
|
||||
name: Extract air before HRC
|
||||
- entity: sensor.penguin_waste_air
|
||||
name: Waste air
|
||||
- entity: sensor.penguin_extract_air_humidity
|
||||
name: Extract air humidity
|
||||
- entity: sensor.penguin_extract_air_humidity_48h
|
||||
name: Extract air humidity 48h
|
||||
name: Extract air humidity 48h avg
|
||||
- entity: sensor.penguin_hrc_efficiency_intake
|
||||
icon: ''
|
||||
name: HRC Efficiency intake
|
||||
name: HRC efficiency intake
|
||||
- entity: sensor.penguin_hrc_efficiency_extract
|
||||
name: HRC Efficiency extract
|
||||
- entity: sensor.penguin_intake_fan_pct
|
||||
name: Intake fan speed
|
||||
- entity: sensor.penguin_exhaust_fan_pct
|
||||
name: Exhaust fan speed
|
||||
name: HRC efficiency extract
|
||||
title: Measurements
|
||||
title: Heating & Ventilation
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
views:
|
||||
- title: Pingvin
|
||||
icon: mdi:penguin
|
||||
- title: Penguin
|
||||
cards:
|
||||
- type: vertical-stack
|
||||
cards:
|
||||
|
@ -20,17 +19,6 @@ views:
|
|||
green: 0
|
||||
yellow: 0
|
||||
red: 100
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: input_boolean.penguin_after_heater
|
||||
name: Lämmitys sallittu
|
||||
secondary_info: last-changed
|
||||
- entity: input_boolean.penguin_snc
|
||||
icon: mdi:snowflake-thermometer
|
||||
name: Kesäyöjäähdytys
|
||||
- entity: input_boolean.penguin_fan_control
|
||||
name: Kiertoilman hallinta
|
||||
state_color: true
|
||||
- square: false
|
||||
columns: 4
|
||||
type: grid
|
||||
|
@ -111,14 +99,12 @@ views:
|
|||
entities:
|
||||
- entity: sensor.penguin_operating_mode
|
||||
name: Tila
|
||||
secondary_info: last-changed
|
||||
- entity: sensor.penguin_room_temperature_1
|
||||
name: Huonelämpötila
|
||||
- entity: sensor.penguin_temperature_setting
|
||||
name: Asetettu lämpötila
|
||||
- entity: sensor.penguin_circulation_fan_pct
|
||||
name: Kiertoilma
|
||||
secondary_info: last-changed
|
||||
state_color: false
|
||||
- type: entities
|
||||
entities:
|
||||
|
@ -128,6 +114,8 @@ views:
|
|||
name: Ulkoilma 24h keskiarvo
|
||||
- entity: sensor.penguin_supply_air_hrc
|
||||
name: Tuloilma LTO jälkeen
|
||||
- entity: sensor.penguin_supply_air_humidity
|
||||
name: Tuloilma kosteus
|
||||
- entity: sensor.penguin_supply_air
|
||||
name: Tuloilma
|
||||
- entity: sensor.penguin_return_water
|
||||
|
@ -145,9 +133,4 @@ views:
|
|||
name: LTO hyötysuhde tuloilma
|
||||
- entity: sensor.penguin_hrc_efficiency_extract
|
||||
name: LTO hyötysuhde poistoilma
|
||||
- entity: sensor.penguin_intake_fan_pct
|
||||
name: Puhallin tuloilma
|
||||
- entity: sensor.penguin_exhaust_fan_pct
|
||||
name: Puhallin poistoilma
|
||||
title: Mittaukset
|
||||
title: Lämmitys & IV
|
||||
|
|
|
@ -9,20 +9,11 @@ input_boolean:
|
|||
name: Penguin Overpressure
|
||||
icon: mdi:fireplace
|
||||
penguin_boost:
|
||||
name: Penguin Boost
|
||||
name: Penguin Overpressure
|
||||
icon: mdi:fan-plus
|
||||
penguin_max_cooling:
|
||||
name: Penguin Max Cooling
|
||||
icon: mdi:snowflake
|
||||
penguin_after_heater:
|
||||
name: Penguin After Heater
|
||||
icon: mdi:heating-coil
|
||||
penguin_snc:
|
||||
name: Penguin Summer Night Cooling
|
||||
icon: mdi:snowflake-thermometer
|
||||
penguin_fan_control:
|
||||
name: Penguin Fan Control
|
||||
icon: mdi:fan-alert
|
||||
input_button:
|
||||
penguin_temperature_up:
|
||||
name: Penguin temperature up
|
||||
|
@ -35,5 +26,4 @@ input_number:
|
|||
name: Penguin temperature setting helper
|
||||
min: 20
|
||||
max: 30
|
||||
unit_of_measurement: "°C"
|
||||
step: 0.5
|
||||
unit_of_measurement: "°C"
|
|
@ -28,14 +28,6 @@ rest:
|
|||
value_template: "{{ value_json['fan_pct'] }}"
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:fan
|
||||
- name: "Penguin intake fan pct"
|
||||
value_template: "{{ value_json['fan_pct_in'] }}"
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:fan
|
||||
- name: "Penguin exhaust fan pct"
|
||||
value_template: "{{ value_json['fan_pct_ex'] }}"
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:fan
|
||||
- name: "Penguin HRC efficiency intake"
|
||||
value_template: "{{ value_json['hrc_efficiency_in'] }}"
|
||||
unit_of_measurement: "%"
|
||||
|
@ -44,6 +36,10 @@ rest:
|
|||
value_template: "{{ value_json['hrc_efficiency_ex'] }}"
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:recycle
|
||||
- name: "Penguin days until service"
|
||||
value_template: "{{ value_json['days_until_service'] }}"
|
||||
unit_of_measurement: "pv"
|
||||
icon: mdi:calendar
|
||||
- name: "Penguin supply air"
|
||||
value_template: "{{ value_json['measurements']['supply_heated'] }}"
|
||||
unit_of_measurement: "°C"
|
||||
|
@ -100,18 +96,8 @@ rest:
|
|||
- name: "Penguin overpressure"
|
||||
value_template: "{{ value_json['coils'][3]['value'] }}"
|
||||
icon: mdi:fireplace
|
||||
- name: "Penguin after heater enabled"
|
||||
value_template: "{{ value_json['coils'][54]['value'] }}"
|
||||
icon: mdi:heating-coil
|
||||
- name: "Penguin summer night cooling enabled"
|
||||
value_template: "{{ value_json['coils'][12]['value'] }}"
|
||||
icon: mdi:heating-coil
|
||||
|
||||
template:
|
||||
- sensor:
|
||||
- name: "Penguin temperature delta"
|
||||
state: "{{ (states('sensor.penguin_room_temperature_1')|float(default=0) - states('sensor.penguin_temperature_setting')|float(default=0)) | round(1, default=0) }}"
|
||||
unit_of_measurement: "°C"
|
||||
- binary_sensor:
|
||||
- name: "Penguin max heating enabled"
|
||||
state: "{{ states('input_number.penguin_temperature_setting_helper') > states('sensor.penguin_room_temperature_1') }}"
|
||||
|
@ -132,13 +118,13 @@ rest_command:
|
|||
username: pingvin
|
||||
password: enervent
|
||||
penguin_boost_on:
|
||||
url: https://IP_ADDRESS:8888/api/v1/coils/10/1
|
||||
url: https://192.168.0.210:8888/api/v1/coils/10/1
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
password: enervent
|
||||
penguin_boost_off:
|
||||
url: https://IP_ADDRESS:8888/api/v1/coils/10/0
|
||||
url: https://192.168.0.210:8888/api/v1/coils/10/0
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
|
@ -165,37 +151,25 @@ rest_command:
|
|||
username: pingvin
|
||||
password: enervent
|
||||
penguin_max_cooling_on:
|
||||
url: https://IP_ADDRESS:8888/api/v1/coils/7/1
|
||||
url: https://192.168.0.210:8888/api/v1/coils/7/1
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
password: enervent
|
||||
penguin_max_cooling_off:
|
||||
url: https://IP_ADDRESS:8888/api/v1/coils/7/0
|
||||
url: https://192.168.0.210:8888/api/v1/coils/7/0
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
password: enervent
|
||||
penguin_temperature_up:
|
||||
url: https://IP_ADDRESS:8888/api/v1/temperature/up
|
||||
url: https://192.168.0.210:8888/api/v1/temperature/up
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
password: enervent
|
||||
penguin_temperature_down:
|
||||
url: https://IP_ADDRESS:8888/api/v1/temperature/down
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
password: enervent
|
||||
penguin_heater_enabled:
|
||||
url: https://IP_ADDRESS:8888/api/v1/coils/54/1
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
password: enervent
|
||||
penguin_heater_disabled:
|
||||
url: https://IP_ADDRESS:8888/api/v1/coils/54/0
|
||||
url: https://192.168.0.210:8888/api/v1/temperature/down
|
||||
method: POST
|
||||
verify_ssl: false
|
||||
username: pingvin
|
||||
|
|
228
main.go
228
main.go
|
@ -1,228 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/0ranki/enervent-ctrl/pingvin"
|
||||
"github.com/0ranki/https-go"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Remember to dereference the symbolic links under ./static/html
|
||||
// prior to building the binary e.g. by using tar
|
||||
|
||||
//go:embed static/html/*
|
||||
var static embed.FS
|
||||
|
||||
var (
|
||||
version = "0.2.0"
|
||||
device pingvin.Pingvin
|
||||
config Conf
|
||||
usernamehash [32]byte
|
||||
passwordhash [32]byte
|
||||
)
|
||||
|
||||
type Conf struct {
|
||||
SerialAddress string `yaml:"serial_address"`
|
||||
Port int `yaml:"port"`
|
||||
SslCertificate string `yaml:"ssl_certificate"`
|
||||
SslPrivatekey string `yaml:"ssl_privatekey"`
|
||||
DisableAuth bool `yaml:"disable_auth"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Interval int `yaml:"interval"`
|
||||
EnableMetrics bool `yaml:"enable_metrics"`
|
||||
LogFile string `yaml:"log_file"`
|
||||
LogAccess bool `yaml:"log_access"`
|
||||
Debug bool `yaml:"debug"`
|
||||
ReadOnly bool `yaml:"read_only"`
|
||||
}
|
||||
|
||||
// Start the HTTP server
|
||||
func serve(cert, key *string) {
|
||||
log.Println("Starting service")
|
||||
http.HandleFunc("/api/v1/coils/", authHandlerFunc(coils))
|
||||
http.HandleFunc("/api/v1/status", authHandlerFunc(status))
|
||||
http.HandleFunc("/api/v1/registers/", authHandlerFunc(registers))
|
||||
http.HandleFunc("/api/v1/temperature/", authHandlerFunc(temperature))
|
||||
if config.EnableMetrics {
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
}
|
||||
html, err := fs.Sub(static, "static/html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
htmlroot := http.FileServer(http.FS(html))
|
||||
http.HandleFunc("/", authHandler(htmlroot))
|
||||
http.HandleFunc("/coils/", authHandler(http.StripPrefix("/coils/", htmlroot)))
|
||||
http.HandleFunc("/registers/", authHandler(http.StripPrefix("/registers/", htmlroot)))
|
||||
logdst, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModeAppend)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if config.LogAccess {
|
||||
logdst = os.Stdout
|
||||
}
|
||||
handler := handlers.LoggingHandler(logdst, http.DefaultServeMux)
|
||||
err = http.ListenAndServeTLS(":8888", *cert, *key, handler)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate self-signed SSL keypair
|
||||
func generateCertificate(cert, key string) {
|
||||
opts := https.GenerateOptions{Host: "enervent-ctrl.local", RSABits: 4096, ValidFor: 10 * 365 * 24 * time.Hour}
|
||||
log.Println("Generating new self-signed SSL keypair")
|
||||
log.Println("This may take a while...")
|
||||
pub, priv, err := https.GenerateKeys(opts)
|
||||
if err != nil {
|
||||
log.Fatal("Error generating SSL certificate: ", err)
|
||||
}
|
||||
device.Debug.Println("Certificate:\n", string(pub))
|
||||
device.Debug.Println("Key:\n", string(priv))
|
||||
if err := os.WriteFile(key, priv, 0600); err != nil {
|
||||
log.Fatal("Error writing private key ", key, ": ", err)
|
||||
}
|
||||
log.Println("Wrote new SSL private key ", cert)
|
||||
if err := os.WriteFile(cert, pub, 0644); err != nil {
|
||||
log.Fatal("Error writing certificate ", cert, ": ", err)
|
||||
}
|
||||
log.Println("Wrote new SSL public key ", cert)
|
||||
}
|
||||
|
||||
// Read & parse the configuration file
|
||||
func parseConfigFile() {
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatal("Could not determine user home directory")
|
||||
}
|
||||
confpath := homedir + "/.config/enervent-ctrl"
|
||||
if _, err := os.Stat(confpath); err != nil {
|
||||
log.Println("Generating configuration directory", confpath)
|
||||
if err := os.MkdirAll(confpath, 0700); err != nil {
|
||||
log.Fatal("Failed to generate configuration directory:", err)
|
||||
}
|
||||
}
|
||||
conffile := confpath + "/configuration.yaml"
|
||||
yamldata, err := os.ReadFile(conffile)
|
||||
if err != nil {
|
||||
log.Println("Configuration file", conffile, "not found")
|
||||
log.Println("Generating", conffile, "with default values")
|
||||
initDefaultConfig(confpath)
|
||||
if yamldata, err = os.ReadFile(conffile); err != nil {
|
||||
log.Fatal("Error parsing configuration:", err)
|
||||
}
|
||||
}
|
||||
err = yaml.Unmarshal(yamldata, &config)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse YAML:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write the default configuration to $HOME/.config/enervent-ctrl/configuration.yaml
|
||||
func initDefaultConfig(confpath string) {
|
||||
config = Conf{
|
||||
SerialAddress: "/dev/ttyS0",
|
||||
Port: 8888,
|
||||
SslCertificate: confpath + "/certificate.pem",
|
||||
SslPrivatekey: confpath + "/privatekey.pem",
|
||||
DisableAuth: false,
|
||||
Username: "pingvin",
|
||||
Password: "enervent",
|
||||
Interval: 4,
|
||||
EnableMetrics: false,
|
||||
LogAccess: false,
|
||||
LogFile: "",
|
||||
Debug: false,
|
||||
ReadOnly: false,
|
||||
}
|
||||
conffile := confpath + "/configuration.yaml"
|
||||
confbytes, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
log.Println("Error writing default configuration:", err)
|
||||
}
|
||||
if err := os.WriteFile(conffile, confbytes, 0600); err != nil {
|
||||
log.Fatal("Failed to write default configuration:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Read configuration. CLI flags take precedence over configuration file
|
||||
func configure() {
|
||||
log.Println("Reading configuration")
|
||||
parseConfigFile()
|
||||
debugflag := flag.Bool("debug", config.Debug, "Enable debug logging")
|
||||
intervalflag := flag.Int("interval", config.Interval, "Set the interval of background updates")
|
||||
logaccflag := flag.Bool("httplog", config.LogAccess, "Enable HTTP access logging")
|
||||
generatecert := flag.Bool("regenerate-certs", false, "Generate a new SSL certificate. A new one is generated on startup as `~/.config/enervent-ctrl/server.crt` if it doesn't exist.")
|
||||
certflag := flag.String("cert", config.SslCertificate, "Path to SSL public key to use for HTTPS")
|
||||
keyflag := flag.String("key", config.SslPrivatekey, "Path to SSL private key to use for HTTPS")
|
||||
noauthflag := flag.Bool("disable-auth", config.DisableAuth, "Disable HTTP basic authentication")
|
||||
usernflag := flag.String("username", config.Username, "Username for HTTP Basic Authentication")
|
||||
passwflag := flag.String("password", config.Password, "Password for HTTP Basic Authentication")
|
||||
promflag := flag.Bool("enable-metrics", config.EnableMetrics, "Enable the built-in Prometheus exporter")
|
||||
logflag := flag.String("logfile", config.LogFile, "Path to log file. Default is empty string, log to stdout")
|
||||
serialflag := flag.String("serial", config.SerialAddress, "Path to serial console for RS-485 connection. Defaults to /dev/ttyS0")
|
||||
readOnly := flag.Bool("read-only", config.ReadOnly, "Read only mode, no writes to device are allowed")
|
||||
// TODO: log file flag
|
||||
flag.Parse()
|
||||
config.Debug = *debugflag
|
||||
config.Interval = *intervalflag
|
||||
config.LogAccess = *logaccflag
|
||||
config.SslCertificate = *certflag
|
||||
config.SslPrivatekey = *keyflag
|
||||
config.DisableAuth = *noauthflag
|
||||
config.Username = *usernflag
|
||||
config.Password = *passwflag
|
||||
config.EnableMetrics = *promflag
|
||||
config.LogFile = *logflag
|
||||
config.SerialAddress = *serialflag
|
||||
config.ReadOnly = *readOnly
|
||||
usernamehash = sha256.Sum256([]byte(config.Username))
|
||||
passwordhash = sha256.Sum256([]byte(config.Password))
|
||||
if len(config.LogFile) != 0 {
|
||||
logfile, err := os.OpenFile(config.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0640)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to open log file", config.LogFile)
|
||||
}
|
||||
log.SetOutput(logfile)
|
||||
log.Println("Opened logfile")
|
||||
}
|
||||
// Check that certificate file exists, generate if needed
|
||||
if _, err := os.Stat(config.SslCertificate); err != nil || *generatecert {
|
||||
generateCertificate(config.SslCertificate, config.SslPrivatekey)
|
||||
}
|
||||
// Enable debug if configured
|
||||
if config.Debug {
|
||||
log.Println("Debug logging enabled")
|
||||
}
|
||||
// Enable HTTP access logging if configured
|
||||
if config.LogAccess {
|
||||
log.Println("HTTP Access logging enabled")
|
||||
}
|
||||
log.Println("Update interval set to", config.Interval, "seconds")
|
||||
if config.EnableMetrics {
|
||||
log.Println("Prometheus exporter enabled (/metrics)")
|
||||
prometheus.MustRegister(&device)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("enervent-ctrl version", version)
|
||||
configure()
|
||||
device = *pingvin.New(config.SerialAddress, config.Debug)
|
||||
device.Update()
|
||||
go device.Monitor(config.Interval)
|
||||
serve(&config.SslCertificate, &config.SslPrivatekey)
|
||||
device.Quit()
|
||||
}
|
Loading…
Reference in New Issue