Compare commits

...

67 Commits

Author SHA1 Message Date
Jarno Rankinen 4045de3f15 Update README.md 2024-07-03 14:28:54 +03:00
Jarno Rankinen d5e87bd073 Update README.md 2024-07-03 14:21:31 +03:00
Jarno Rankinen 26c16e7c8e Update README.md 2024-07-03 14:21:01 +03:00
Jarno Rankinen 972398d8ea v0.2.0, update HA-related instructions in Readme 2024-04-07 12:44:51 +03:00
Jarno Rankinen 8c825580fa Placeholder IP_ADDRESS for daemon ip address in HA rest configuration 2024-04-07 12:24:19 +03:00
Jarno Rankinen 6431315462 gh-32 Update HA automation, helper and rest endpoint definitions
- Added automations to alter the behaviour of the Pingvin unit
2024-04-07 12:02:13 +03:00
Jarno Rankinen 11debcecc5 gh-32 Update HA dashboard definitions
- Added toggles for Summer Night Cooling and toggle
  for disabling HA changing fan speeds
- Removed supply air humidity, probably needs an optional
  extra sensor to work
2024-04-07 11:14:01 +03:00
Jarno Rankinen 2c00babf4d gh-32 Supply/Exhaust fan speeds to status endpoint 2024-04-07 11:00:17 +03:00
Jarno Rankinen cf22dcf6f9 docs: Mention more tested devices and the dialout group 2024-03-19 22:38:41 +02:00
Jarno Rankinen 86afcef6ec Update build pipeline for Go 1.22 2024-03-19 22:23:57 +02:00
Jarno Rankinen 25887e1111 v0.1.3 Module updates
- Go update to v1.22
- github.com/golang/protobuf v1.5.2 -> google.golang.org/protobuf v1.33.0
- github.com/gorilla/handlers v1.5.1 -> v1.5.2
- github.com/prometheus/client_golang v1.14.0 -> v1.19.0
- github.com/cespare/xxhash/v2 v2.1.2 -> v2.2.0
- github.com/felixge/httpsnoop v1.0.1 -> v1.0.4
- github.com/matttproud/golang_protobuf_extensions v1.0.1 -> v1.0.4
- github.com/prometheus/client_model v0.3.0 -> v0.6.0
- github.com/prometheus/common v0.37.0 -> v0.50.0
- github.com/prometheus/procfs v0.8.0 -> v0.13.0
- golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a -> v0.18.0
- golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
  -> v0.0.0-20231012003039-104605ab7028
2024-03-19 22:16:22 +02:00
Jarno Rankinen ef6627dae8 ci: Add 'fetch-depth: 0' to build checkout action, probably fixes changelog for releases 2024-03-08 00:29:39 +02:00
Jarno Rankinen d1f734dd32 v0.1.2
- Fix routing, remove the awkward symlinking system for index.html
- Automatic binary build with GH Actions
2024-03-08 00:20:02 +02:00
Jarno Rankinen 8278bc9445 Automatic build action, take 1 2024-03-07 23:03:33 +02:00
Jarno Rankinen 28a555fa2b Don't exit on max retries in updateRegisters, except on initial run 2024-03-07 22:54:24 +02:00
Jarno Rankinen ea8ca1a6df Don't exit on max retries in updateRegisters 2024-03-07 22:38:18 +02:00
Jarno Rankinen d9fbbfac1c Fix typos 2024-03-07 21:20:40 +02:00
Jarno Rankinen 5458c3ba86 v0.1.0 2024-01-21 11:39:36 +02:00
Jarno Rankinen 027d678a09 v0.0.29 Improved error handling
- Retry mechanic for coil read errors
- Utilize pointers, attempt to have more persistent state
- Improved error handling and slightly more verbose logging
2024-01-21 11:34:45 +02:00
Jarno Rankinen 32fa6d4321 Update README.md 2024-01-14 17:06:46 +02:00
Jarno Rankinen 0aeb54dbd8 Update README.md 2024-01-14 17:04:35 +02:00
Jarno Rankinen 1a4b22df02 Updated readme for v0.0.28 2023-10-09 21:39:12 +03:00
Jarno Rankinen 19e047b142 v0.0.28 Added --read-only flag & read_only: true|false config option. Closes gh-33 2023-10-09 21:33:53 +03:00
Jarno Rankinen efbfe72bba Disable Add to project action 2023-10-09 21:12:45 +03:00
Jarno Rankinen af4405550c v0.0.27 Added --disable-auth flag and disable_auth: true|false config options. Disables HTTP authentication. build script added to repo. Closes gh-31 2023-10-09 21:08:26 +03:00
Jarno Rankinen 951e3ba493 v0.0.26 - Usable on a daily basis
- Added 'Temperature delta' template sensor, measures difference
  between Room temp vs Temp setpoint (+1.0 = room is 1 degree above
  setpoint)
- Added input helper, rest commands and binary sensor for disabling/enabling
  after heater element
- Updated English dashboard to match Finnish one
- Alert support is the major thing missing
- Automations to override Pingvin's own are under testing
2023-04-07 22:26:14 +03:00
Jarno Rankinen 87482c793a
Physical connection instructions 2023-04-04 22:41:47 +03:00
Jarno Rankinen 19d4d3d78b
images continued 2023-04-04 22:40:50 +03:00
Jarno Rankinen 1d1d2990a4
images 2023-04-04 22:21:25 +03:00
Jarno Rankinen f31cf085ca
install instructions continued 2023-04-04 22:11:45 +03:00
Jarno Rankinen 5a1f0d298f
Update README.md
Install instructions 1
2023-04-04 21:53:06 +03:00
Jarno Rankinen 789a92e2e4
Update README.md 2023-03-30 15:42:45 +03:00
Jarno Rankinen 9cfd61448d
Update README.md 2023-03-30 15:41:34 +03:00
Jarno Rankinen 11f2df4f6b
Update README.md 2023-03-30 15:24:10 +03:00
Jarno Rankinen 0231364589
Update README.md 2023-03-30 15:23:26 +03:00
Jarno Rankinen 2c12a88c6d
Update README.md 2023-03-30 15:22:55 +03:00
Jarno Rankinen 9b51caac9f
Update README.md 2023-03-30 15:21:59 +03:00
Jarno Rankinen f6e5039af6
Create LICENSE 2023-03-30 15:05:41 +03:00
Jarno Rankinen 5ecbe2a038
Update README.md 2023-03-30 15:00:16 +03:00
Jarno Rankinen 643eae3e5d Update README 2023-03-30 14:57:57 +03:00
Jarno Rankinen df42907f65
Update README.md 2023-03-28 22:16:36 +03:00
Jarno Rankinen 2a76c9a413 Configuring the serial path actually does something now 2023-03-20 20:55:20 +02:00
Jarno Rankinen 545897211e Ability to configure serial path 2023-03-20 20:29:00 +02:00
Jarno Rankinen ed67cac11c Updated README.md after removing Python sources 2023-03-15 22:19:50 +02:00
Jarno Rankinen a2970f1754 0.0.24 2023-03-15 22:09:06 +02:00
Jarno Rankinen 28548005b8 Remove unused statuslock Mutex 2023-03-15 21:42:16 +02:00
Jarno Rankinen c10df356d1 Pingvin.New returns pointer to instance 2023-03-15 21:40:37 +02:00
Jarno Rankinen c0798af980 Renamed PingvinKL type to Pingvin 2023-03-15 21:39:14 +02:00
Jarno Rankinen cb1b818b65 Renamed pingvin variable -> device 2023-03-15 21:35:23 +02:00
Jarno Rankinen 140e68aa13 Renamed packages 2023-03-15 21:29:57 +02:00
Jarno Rankinen 5224f92516 Moved Go files under repo root 2023-03-15 21:20:48 +02:00
Jarno Rankinen 2739a3d984 Combined .gitignores 2023-03-15 21:19:28 +02:00
Jarno Rankinen 208d7d05d7 Moved Python stuff to dedicated repo, preparing to clean up packagenaming 2023-03-15 21:17:59 +02:00
Jarno Rankinen ea0a62598a gh-28 Logfile configuration option and cli flag. Empty string means log to STDOUT. Closes gh-28 2023-03-14 23:44:04 +02:00
Jarno Rankinen 323c76af19 Squashed commit of the following:
commit 4062e62071
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Tue Mar 14 23:27:49 2023 +0200

    gh-3 config file entry and cli flag to enable the Prometheus exporter. Disabled by default.

commit df9e6d5471
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Tue Mar 14 23:13:36 2023 +0200

    gh-3 Prometheus exporter for unreserved coils

Closes gh-3
2023-03-14 23:28:09 +02:00
Jarno Rankinen 02312c522b Squashed commit of the following:
commit 4b3f7c6f45
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Tue Mar 14 12:36:03 2023 +0200

    Configuration file ~/.config/enervent-ctrl/configuration.yaml

    If the file does not exist, will be generated with default values.
    CLI flags override values from configuration file.

Closes gh-19
2023-03-14 12:37:34 +02:00
Jarno Rankinen d53e976a36 Squashed commit of the following:
commit 468bc33f7e
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Tue Mar 14 09:37:07 2023 +0200

    gh-7 Log failed authentications. Forked rocketlaunchr/https-go to add CN to the generated certifcate

commit 0e10c9d925
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Sun Mar 12 22:30:53 2023 +0200

    gh-7 Added HTTP basic authentication to REST API. Default credentials pingvin:enervent. Still need to implement logging failed auth requests. A log file option is also a good idea.

commit a3165db631
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Sun Mar 12 12:41:41 2023 +0200

    gh-7 Updated Home Assistant rest example to https

commit 54b555c523
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Sun Mar 12 12:25:53 2023 +0200

    gh-7 Enabled SSL encryption

commit 2738b60386
Author: Jarno Rankinen <jarno@oranki.net>
Date:   Sun Mar 12 12:07:36 2023 +0200

    gh-7 Implemented automatic self-signed SSL generation on startup under ~/.config/enervent-ctrl

Closes gh-7
2023-03-14 09:37:36 +02:00
Jarno Rankinen 0311605bae
Update README.md
Add mention about copyright.
2023-03-11 12:28:02 +02:00
Jarno Rankinen f95004a7bc
Update README.md 2023-03-11 12:20:08 +02:00
Jarno Rankinen 534fb5939d Implemented max cooling toggle to dashboard. Max heating is shown when the requested temperature is less than room temperature, otherwise Max cooling toggle is shown. Closes gh-22 2023-03-10 21:05:52 +02:00
Jarno Rankinen ec402259f4 Rename build directory to BUILD 2023-03-10 14:15:56 +02:00
Jarno Rankinen 778c249b9b gh-22 gh-6 ADashboard, rest entities, helpers and automations for boost and overpressure 2023-03-10 14:13:26 +02:00
Jarno Rankinen a5fbc53eba gh-6 Rework automations, as homeassistant.update_entity doesn't seem to be working for restful resources with multiple sensors 2023-03-10 13:47:34 +02:00
Jarno Rankinen c6741645b6 gh-6 update dashboard, helpers and automations for temperature setting via helper+automations.
Will postpone updating the english dashboard until most of the features are
done in the finnish version.
2023-03-10 12:03:52 +02:00
Jarno Rankinen 4b89aec1c5 0.0.17 Remove 'Days until service'. Closes gh-25 2023-03-10 11:13:07 +02:00
Jarno Rankinen a0dedc7fcd 0.0.16 Correct hreg addresses for humidity readings. Outside hum still shows 0%, will need to wait until summer to see if the reading works. Closes gh-26. Added 48h avg hum readings back to HASS dashboards 2023-03-10 11:09:36 +02:00
Jarno Rankinen a0bb5c3e69 temperature endpoint can take raw register value, full degrees C or a single decimal precision float degrees C value as parameter. up & down as parameters shift the temperature setpoint up or down a full degree. Closes gh-21 2023-03-09 22:35:43 +02:00
41 changed files with 2304 additions and 844 deletions

View File

@ -1,9 +1,9 @@
name: Add issues to project
on:
issues:
types:
- opened
#on:
# issues:
# types:
# - opened
jobs:
add-to-project:

30
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,30 @@
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 }}

3
.gitignore vendored
View File

@ -1 +1,4 @@
.vscode/
.idea/
BUILD/*
TMP/*

32
.goreleaser.yaml Normal file
View File

@ -0,0 +1,32 @@
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 Normal file
View File

@ -0,0 +1,21 @@
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.

167
README.md
View File

@ -4,20 +4,161 @@ External control of an Enervent Pingvin
Kotilämpö residential heating/ventilation
unit via RS485 bus using the Modbus protocol.
Work is my Bachelor's Thesis at Oulu University
of Applied Sciences, software development.
Provides a REST API for integration into Home Assistant,
with measurements and basic control over Pingvin functions.
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`
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 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
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 to make sure the daemon stays as
lightweight as possible.
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
of Applied Sciences.
Pingvin and Kotilämpö are registered trademarks of Enervent Zehnder Oy.
<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>

15
build.sh Executable file
View File

@ -0,0 +1,15 @@
#!/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" .

View File

@ -36,7 +36,7 @@
35;-;-
36;COIL_TEMP_DECREASE;Temperature decrease function desc
37;COIL_OVERTIME;Programmatic equivalent of OVERTIME digital input.
38;-;Emergency stop switch type desc
38;COIL_EMERG_STOP;Emergency stop switch type desc
39;-;-
40;COIL_ECO_MODE;Eco mode desc
41;COIL_ALARM_A;Alarm of class A active desc
1 0 COIL_STOP Stop the machine
36 35 - -
37 36 COIL_TEMP_DECREASE Temperature decrease function desc
38 37 COIL_OVERTIME Programmatic equivalent of OVERTIME digital input.
39 38 - COIL_EMERG_STOP Emergency stop switch type desc
40 39 - -
41 40 COIL_ECO_MODE Eco mode desc
42 41 COIL_ALARM_A Alarm of class A active desc

View File

@ -1,2 +0,0 @@
build.sh
build/*

View File

@ -1,13 +0,0 @@
module github.com/0ranki/enervent-ctrl/enervent-ctrl-go
go 1.18
require (
github.com/goburrow/modbus v0.1.0
github.com/gorilla/handlers v1.5.1
)
require (
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/goburrow/serial v0.1.0 // indirect
)

View File

@ -1,8 +0,0 @@
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=

View File

@ -1,170 +0,0 @@
package main
import (
"embed"
"encoding/json"
"flag"
"io/fs"
"log"
"net/http"
"os"
"strconv"
"strings"
"github.com/0ranki/enervent-ctrl/enervent-ctrl-go/pingvinKL"
"github.com/gorilla/handlers"
)
// 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.14"
pingvin pingvinKL.PingvinKL
DEBUG *bool
INTERVAL *int
ACCESS_LOG *bool
)
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])
}
}
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])
}
}
func status(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(pingvin.Status)
}
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
}
}
func listen() {
log.Println("Starting pingvinAPI...")
http.HandleFunc("/api/v1/coils/", coils)
http.HandleFunc("/api/v1/registers/", registers)
http.HandleFunc("/api/v1/status", status)
http.HandleFunc("/api/v1/temperature/", temperature)
html, err := fs.Sub(static, "static/html")
if err != nil {
log.Fatal(err)
}
htmlroot := http.FileServer(http.FS(html))
http.Handle("/", htmlroot)
if *ACCESS_LOG {
handler := handlers.LoggingHandler(os.Stdout, http.DefaultServeMux)
err = http.ListenAndServe(":8888", handler)
} else {
err = http.ListenAndServe(":8888", nil)
}
if err != nil {
log.Fatal(err)
}
}
func configure() {
log.Println("Reading configuration")
DEBUG = flag.Bool("debug", false, "Enable debug logging")
INTERVAL = flag.Int("interval", 4, "Set the interval of background updates")
ACCESS_LOG = flag.Bool("httplog", false, "Enable HTTP access logging")
flag.Parse()
if *DEBUG {
log.Println("Debug logging enabled")
}
if *ACCESS_LOG {
log.Println("HTTP Access logging enabled")
}
log.Println("Update interval set to", *INTERVAL, "seconds")
}
func main() {
log.Println("enervent-ctrl version", version)
configure()
pingvin = pingvinKL.New(*DEBUG)
pingvin.Update()
go pingvin.Monitor(*INTERVAL)
listen()
pingvin.Quit()
}

View File

@ -1 +0,0 @@
../index.html

View File

@ -1 +0,0 @@
../index.html

View File

@ -1,6 +0,0 @@
*/__pycache__/
bin/
include/
lib/
lib64
share/

View File

@ -1 +0,0 @@
../index.html

View File

@ -1,16 +0,0 @@
<!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>

View File

@ -1 +0,0 @@
../index.html

View File

@ -1,17 +0,0 @@
.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;
}

View File

@ -1,52 +0,0 @@
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);
}

View File

@ -1,59 +0,0 @@
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;
}
}

View File

@ -1,3 +0,0 @@
home = /usr/bin
include-system-site-packages = false
version = 3.9.2

View File

@ -1,11 +0,0 @@
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

View File

@ -1,188 +0,0 @@
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)

View File

@ -1,47 +0,0 @@
#!/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 Normal file
View File

@ -0,0 +1,25 @@
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 Normal file
View File

@ -0,0 +1,520 @@
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 Normal file
View File

@ -0,0 +1,185 @@
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
}
}

View File

@ -1,90 +1,572 @@
automation:
- 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"
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: {}
- delay:
hours: 0
minutes: 0
seconds: 0
milliseconds: 500
- service: homeassistant.update_entity
data: {}
- if:
- condition: state
entity_id: binary_sensor.penguin_max_heating
state: "on"
then: []
else:
- 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
## 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
- alias: Penguin circulation fan mode
description: ""
trigger:
- platform: state
entity_id:
- input_boolean.penguin_circulation_fan_adaptive
condition: []
action:
- if:
- condition: state
## 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
state: "on"
then:
- service: rest_command.penguin_circulation_adaptive
data: {}
- if:
- condition: state
else:
- service: input_boolean.turn_off
data: { }
target:
entity_id: input_boolean.penguin_circulation_fan_adaptive
state: "off"
then:
- service: rest_command.penguin_circulation_manual
data: {}
mode: single
- 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
description: ""
trigger:
- platform: state
entity_id:
- input_boolean.penguin_max_cooling
condition: [ ]
action:
- if:
- condition: state
entity_id: input_boolean.penguin_max_cooling
state: "on"
- condition: state
entity_id: binary_sensor.penguin_max_cooling
state: "off"
- condition: numeric_state
entity_id: sensor.penguin_room_temperature_1
above: input_number.penguin_temperature_setting_helper
then:
- service: rest_command.penguin_max_cooling_on
data: { }
else: [ ]
- if:
- condition: state
entity_id: input_boolean.penguin_max_cooling
state: "off"
then:
- service: rest_command.penguin_max_cooling_off
data: { }
- if:
- condition: numeric_state
entity_id: sensor.penguin_room_temperature_1
below: input_number.penguin_temperature_setting_helper
- condition: state
entity_id: input_boolean.penguin_max_cooling
state: "on"
then:
- service: input_boolean.turn_off
data: { }
target:
entity_id: input_boolean.penguin_max_cooling
mode: single
- alias: Penguin max cooling sensor v0.2.0
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 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
description: ""
trigger:
- platform: state
entity_id:
- input_button.penguin_temperature_down
condition: [ ]
action:
- service: input_number.decrement
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 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
description: ""
trigger:
- platform: state
entity_id:
- sensor.penguin_temperature_setting
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

View File

@ -1,5 +1,6 @@
views:
- title: Penguin
- title: Pingvin
icon: mdi:penguin
cards:
- type: vertical-stack
cards:
@ -19,11 +20,22 @@ 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: 3
columns: 4
type: grid
cards:
- show_name: true
- show_name: false
show_icon: true
type: button
tap_action:
@ -31,49 +43,111 @@ views:
entity: input_boolean.penguin_circulation_fan_adaptive
name: Adaptive circulation
show_state: false
- show_name: true
- 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_icon: true
type: button
tap_action:
action: toggle
entity: input_boolean.penguin_max_heating
name: Max heating
show_state: false
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
- type: entities
entities:
- entity: sensor.penguin_operating_mode
name: Mode
name: Tila
secondary_info: last-changed
- entity: sensor.penguin_room_temperature_1
name: Room temperature
- entity: sensor.penguin_temperature_setting
name: Temperature setpoint
- entity: sensor.penguin_circulation_fan_pct
name: Criculation fan
- entity: sensor.penguin_days_until_service
name: Next maintenance
name: Circulation fan pct
secondary_info: last-changed
state_color: false
- type: entities
entities:
- entity: sensor.penguin_intake_air
name: Outside air at machine
name: Outside air at unit
- entity: sensor.penguin_intake_air_24h
name: Outside air 24h avg
- entity: sensor.penguin_supply_air_hrc
name: Supply air after HRC
- entity: sensor.penguin_supply_air_humidity
name: Supply air humidity
name: Intake after HRC
- entity: sensor.penguin_supply_air
name: Supply air
- entity: sensor.penguin_return_water
name: Return water
name: Return water temperature
- entity: sensor.penguin_extract_air
name: Extract air before HRC
name: Extract 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
- entity: sensor.penguin_hrc_efficiency_intake
name: HRC efficiency intake
icon: ''
name: HRC Efficiency intake
- entity: sensor.penguin_hrc_efficiency_extract
name: 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
title: Measurements
title: Heating & Ventilation

View File

@ -1,5 +1,6 @@
views:
- title: Penguin
- title: Pingvin
icon: mdi:penguin
cards:
- type: vertical-stack
cards:
@ -19,11 +20,22 @@ 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: 3
columns: 4
type: grid
cards:
- show_name: true
- show_name: false
show_icon: true
type: button
tap_action:
@ -31,26 +43,82 @@ views:
entity: input_boolean.penguin_circulation_fan_adaptive
name: Mukautuva kiertoilma
show_state: false
- show_name: true
- 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 lämmitys
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 jäähdytys
show_state: false
- show_name: false
show_icon: true
type: button
tap_action:
action: toggle
entity: input_boolean.penguin_max_heating
name: Max lämmitys
show_state: false
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
- type: entities
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
- entity: sensor.penguin_days_until_service
name: Seuraava huolto
secondary_info: last-changed
state_color: false
- type: entities
entities:
@ -60,8 +128,6 @@ 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
@ -72,9 +138,16 @@ views:
name: Jäteilma
- entity: sensor.penguin_extract_air_humidity
name: Poistoilma kosteus
- entity: sensor.penguin_extract_air_humidity_48h
name: Poistoilma kosteus 48h
- entity: sensor.penguin_hrc_efficiency_intake
icon: ''
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

View File

@ -4,4 +4,36 @@ input_boolean:
icon: mdi:heat-wave
penguin_circulation_fan_adaptive:
name: Penguin Adaptive Circulation Fan
icon: mdi:fan-auto
icon: mdi:fan-auto
penguin_overpressure:
name: Penguin Overpressure
icon: mdi:fireplace
penguin_boost:
name: Penguin Boost
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
icon: mdi:thermometer-plus
penguin_temperature_down:
name: Penguin temperature down
icon: mdi:thermometer-minus
input_number:
penguin_temperature_setting_helper:
name: Penguin temperature setting helper
min: 20
max: 30
unit_of_measurement: "°C"
step: 0.5

View File

@ -1,6 +1,9 @@
rest:
- resource: http://IP_ADDRESS:8888/api/v1/status
scan_interval: 15
- resource: https://IP_ADDRESS:8888/api/v1/status
scan_interval: 5
verify_ssl: false
username: pingvin
password: enervent
sensor:
- name: "Penguin operating mode"
value_template: "{{ value_json['op_mode'] }}"
@ -25,6 +28,14 @@ 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: "%"
@ -33,10 +44,6 @@ 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"
@ -90,33 +97,106 @@ rest:
- name: "Penguin boost"
value_template: "{{ value_json['coils'][10]['value'] }}"
icon: mdi:fan-plus
- 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') }}"
rest_command:
penguin_circulation_adaptive:
url: http://IP_ADDRESS:8888/api/v1/coils/11/1
url: https://IP_ADDRESS:8888/api/v1/coils/11/1
method: POST
icon: mdi:fan-auto
verify_ssl: false
username: pingvin
password: enervent
penguin_circulation_manual:
url: http://IP_ADDRESS:8888/api/v1/coils/11/0
url: https://IP_ADDRESS:8888/api/v1/coils/11/0
method: POST
icon: mdi:fan
penguin_boost_toggle:
url: http://IP_ADDRESS:8888/api/v1/coils/10
verify_ssl: false
username: pingvin
password: enervent
penguin_boost_on:
url: https://IP_ADDRESS:8888/api/v1/coils/10/1
method: POST
icon: mdi:fan-plus
verify_ssl: false
username: pingvin
password: enervent
penguin_boost_off:
url: https://IP_ADDRESS:8888/api/v1/coils/10/0
method: POST
verify_ssl: false
username: pingvin
password: enervent
penguin_overpressure_toggle:
url: http://IP_ADDRESS:8888/api/v1/coils/3
url: https://IP_ADDRESS:8888/api/v1/coils/3
method: POST
icon: mdi:arrow-expand-all
verify_ssl: false
username: pingvin
password: enervent
penguin_max_heating_on:
url: http://IP_ADDRESS:8888/api/v1/coils/6/1
url: https://IP_ADDRESS:8888/api/v1/coils/6/1
method: POST
icon: mdi:heat-wave
verify_ssl: false
username: pingvin
password: enervent
penguin_max_heating_off:
url: http://IP_ADDRESS:8888/api/v1/coils/6/0
url: https://IP_ADDRESS:8888/api/v1/coils/6/0
method: POST
icon: mdi:scent-off
penguin_max_cooling_toggle:
url: http://IP_ADDRESS:8888/api/v1/coils/7
verify_ssl: false
username: pingvin
password: enervent
penguin_max_cooling_on:
url: https://IP_ADDRESS:8888/api/v1/coils/7/1
method: POST
icon: mdi:snowflake
verify_ssl: false
username: pingvin
password: enervent
penguin_max_cooling_off:
url: https://IP_ADDRESS: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
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
method: POST
verify_ssl: false
username: pingvin
password: enervent

228
main.go Normal file
View File

@ -0,0 +1,228 @@
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()
}

View File

@ -1,4 +1,4 @@
package pingvinKL
package pingvin
import (
"bufio"
@ -11,39 +11,42 @@ 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"`
Address int `json:"address"`
Symbol string `json:"symbol"`
Value bool `json:"value"`
Description string `json:"description"`
Reserved bool `json:"reserved"`
PromDesc *prometheus.Desc `json:"-"`
}
// unit modbus data
type PingvinKL struct {
Coils []pingvinCoil
Registers []pingvinRegister
Status pingvinStatus
buslock *sync.Mutex
statuslock *sync.Mutex
handler *modbus.RTUClientHandler
modbusclient modbus.Client
Debug PingvinLogger
type Pingvin struct {
Coils []*pingvinCoil
Registers []*pingvinRegister
Status *pingvinStatus
buslock *sync.Mutex
handler *modbus.RTUClientHandler
modbusclient modbus.Client
firstReadDone bool
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"`
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:"-"`
}
type pingvinMeasurements struct {
@ -61,18 +64,19 @@ type pingvinMeasurements struct {
}
type pingvinStatus struct {
HeaterPct int `json:"heater_pct"` // After heater valve position
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
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
DaysUntilService int `json:"days_until_service"` // Days until next filter service
Uptime string `json:"uptime"` // Unit uptime
SystemTime string `json:"system_time"` // Time and date in unit
Coils []pingvinCoil `json:"coils"`
HeaterPct int `json:"heater_pct"` // After heater valve position
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"`
}
type PingvinLogger struct {
@ -100,17 +104,29 @@ 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 == "-"
coil := pingvinCoil{addr, symbol, false, description, reserved}
return coil
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}
}
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)
@ -123,8 +139,29 @@ func newRegister(address, symbol, typ, multiplier, description string) pingvinRe
}
}
reserved := symbol == "Reserved" && description == "Reserved"
register := pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl}
return register
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}
}
// read a CSV file containing data for coils or registers
@ -149,9 +186,10 @@ func readCsvLines(file string) [][]string {
// Create modbus.Handler, store it in p.handler,
// connect the handler and create p.modbusclient (modbus.Client)
func (p *PingvinKL) createModbusClient() {
// TODO: read configuration from file, hardcoded for now
p.handler = modbus.NewRTUClientHandler("/dev/ttyS0")
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)
p.handler.BaudRate = 19200
p.handler.DataBits = 8
p.handler.Parity = "N"
@ -166,7 +204,7 @@ func (p *PingvinKL) createModbusClient() {
p.modbusclient = modbus.NewClient(p.handler)
}
func (p *PingvinKL) Quit() {
func (p *Pingvin) Quit() {
err := p.handler.Close()
if err != nil {
log.Println("ERROR: Quit:", err)
@ -174,12 +212,24 @@ func (p *PingvinKL) Quit() {
}
// Update all coil values
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)
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)
}
// modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7,
// second byte coils 8-15 etc.
@ -202,13 +252,13 @@ func (p *PingvinKL) updateCoils() {
// Read a single holding register, stores value in p.Registers
// Returns integer value of register
func (p *PingvinKL) ReadRegister(addr uint16) (int, error) {
func (p *Pingvin) 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 0, err
//log.Println("ERROR: ReadRegister:", err)
return p.Registers[addr].Value, err
}
if p.Registers[addr].Type == "uint16" {
p.Registers[addr].Value = int(uint16(results[0]) << 8)
@ -221,7 +271,7 @@ func (p *PingvinKL) ReadRegister(addr uint16) (int, error) {
}
// Update a single holding register
func (p *PingvinKL) WriteRegister(addr uint16, value uint16) (uint16, error) {
func (p *Pingvin) WriteRegister(addr uint16, value uint16) (uint16, error) {
p.buslock.Lock()
_, err := p.modbusclient.WriteSingleRegister(addr, value)
p.buslock.Unlock()
@ -235,13 +285,14 @@ func (p *PingvinKL) 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 *PingvinKL) updateRegisters() {
func (p *Pingvin) updateRegisters() {
var err error
regs := len(p.Registers)
k := 0
@ -253,8 +304,8 @@ func (p *PingvinKL) updateRegisters() {
if regs-k < 125 {
r = regs - k
}
results := []byte{}
for retries := 0; retries < 5; retries++ {
var results []byte
for retries := 1; retries <= 5; retries++ {
p.Debug.Println("Reading registers, attempt", retries, "k:", k)
p.buslock.Lock()
results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r))
@ -262,11 +313,18 @@ func (p *PingvinKL) updateRegisters() {
if len(results) > 0 {
break
} else if retries == 4 {
log.Fatal("updateRegisters: client.ReadHoldingRegisters: ", err)
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
} else if err != nil {
log.Println("WARNING: updateRegisters: client.ReadHoldingRegisters: ", err)
log.Printf("WARNING: updateRegisters: client.ReadHoldingRegisters attempt %d: %s", retries, 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
@ -308,29 +366,37 @@ func (p *PingvinKL) updateRegisters() {
// Wrapper function for updating coils, registers and populating
// p.Status for Home Assistant
func (p *PingvinKL) Update() {
func (p *Pingvin) Update() {
p.updateCoils()
p.updateRegisters()
p.populateStatus()
}
// Read single coil
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
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)
}
p.Coils[n].Value = results[0] == 1
return results, nil
return
}
// Force a single coil
func (p *PingvinKL) WriteCoil(n uint16, val bool) bool {
func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
if val {
p.checkMutexCoils(n, p.handler)
_ = p.checkMutexCoils(n) //, p.handler)
}
var value uint16 = 0
if val {
@ -342,19 +408,20 @@ func (p *PingvinKL) WriteCoil(n uint16, val bool) bool {
if err != nil {
log.Println("ERROR: WriteCoil: ", err)
}
if (val && results[0] == 255) || (!val && results[0] == 0) {
log.Println("WriteCoil: wrote coil", n, "to value", val)
} else {
if (val && results[0] != 255) && (!val && results[0] != 0) {
log.Println("ERROR: WriteCoil: failed to write coil")
return false
}
p.ReadCoil(n)
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)
return true
}
// Force multiple coils
func (p *PingvinKL) WriteCoils(startaddr uint16, quantity uint16, vals []bool) error {
func (p *Pingvin) WriteCoils(startaddr uint16, quantity uint16, vals []bool) error {
p.updateCoils()
coilslice := p.Coils[startaddr:(startaddr + quantity)]
if len(coilslice) != len(vals) {
@ -405,7 +472,7 @@ func (p *PingvinKL) WriteCoils(startaddr uint16, quantity uint16, vals []bool) e
// 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 *PingvinKL) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandler) error {
func (p *Pingvin) checkMutexCoils(addr uint16) error { //, handler *modbus.RTUClientHandler) error {
for _, mutexcoil := range mutexcoils {
if mutexcoil == addr {
for _, n := range mutexcoils {
@ -426,7 +493,8 @@ func (p *PingvinKL) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandle
}
// populate p.Status struct for Home Assistant
func (p *PingvinKL) populateStatus() {
func (p *Pingvin) populateStatus() {
p.Status = &pingvinStatus{}
hpct := p.Registers[49].Value / p.Registers[49].Multiplier
if hpct > 100 {
p.Status.HeaterPct = hpct - 100
@ -437,22 +505,23 @@ func (p *PingvinKL) 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)
p.Status.Measurements.SupplyIntake = float32(p.Registers[6].Value) / float32(p.Registers[6].Multiplier)
p.Status.Measurements.SupplyIntake24h = float32(p.Registers[134].Value) / float32(p.Registers[134].Multiplier)
p.Status.Measurements.SupplyHum = float32(p.Registers[36].Value) / float32(p.Registers[46].Multiplier)
p.Status.Measurements.SupplyHum = float32(p.Registers[36].Value) / float32(p.Registers[36].Multiplier)
p.Status.Measurements.Watertemp = float32(p.Registers[12].Value) / float32(p.Registers[12].Multiplier)
p.Status.Measurements.ExtractIntake = float32(p.Registers[10].Value) / float32(p.Registers[10].Multiplier)
p.Status.Measurements.ExtractHrc = float32(p.Registers[9].Value) / float32(p.Registers[9].Multiplier)
p.Status.Measurements.ExtractHum = float32(p.Registers[28].Value) / float32(p.Registers[28].Multiplier)
p.Status.Measurements.ExtractHum48h = float32(p.Registers[50].Value) / float32(p.Registers[50].Multiplier)
p.Status.Measurements.ExtractHum = float32(p.Registers[13].Value) / float32(p.Registers[13].Multiplier)
p.Status.Measurements.ExtractHum48h = float32(p.Registers[35].Value) / float32(p.Registers[35].Multiplier)
p.Status.HrcEffIn = p.Registers[29].Value / p.Registers[29].Multiplier
p.Status.HrcEffEx = p.Registers[30].Value / p.Registers[30].Multiplier
p.Status.OpMode = parseStatus(p.Registers[44].Value)
// TODO: Alarms, n of alarms
p.Status.DaysUntilService = p.Registers[538].Value / p.Registers[538].Multiplier
// TODO: Uptime & date in separate functions
p.Status.Coils = p.Coils
}
@ -487,7 +556,13 @@ func parseStatus(value int) string {
}
func (p *PingvinKL) Temperature(action string) error {
// Change temperature setpoint (register 135)
// action can be up, down or a value.
// If value, the value can be the raw register value (200-300),
// 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 {
temperature := 0
if action == "up" {
temperature = p.Registers[135].Value + 1*p.Registers[135].Multiplier
@ -505,13 +580,13 @@ func (p *PingvinKL) Temperature(action string) error {
return err
}
t = int(tfloat * float64(p.Registers[135].Multiplier))
}
if t <= 30 && t >= 20 {
temperature = 10 * t
} else {
temperature = t
}
p.Debug.Println("Setting temperature to", temperature)
// _, err = p.WriteRegister(135, uint16(t))
// if err != nil {
// return err
// }
}
if temperature > 300 || temperature < 200 {
return fmt.Errorf("Temperature setpoint must be between 200 and 300")
@ -525,7 +600,7 @@ func (p *PingvinKL) Temperature(action string) error {
return nil
}
func (p *PingvinKL) Monitor(interval int) {
func (p *Pingvin) Monitor(interval int) {
for {
time.Sleep(time.Duration(interval) * time.Second)
p.Debug.Println("Updating values")
@ -533,12 +608,52 @@ func (p *PingvinKL) Monitor(interval int) {
}
}
// create a PingvinKL struct, read coils and registers from CSVs
func New(debug bool) PingvinKL {
pingvin := PingvinKL{}
// 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{}
pingvin.Debug.dbg = debug
pingvin.buslock = &sync.Mutex{}
pingvin.createModbusClient()
pingvin.createModbusClient(serial)
log.Println("Parsing coil data...")
coilData := readCsvLines("coils.csv")
for i := 0; i < len(coilData); i++ {
@ -552,5 +667,5 @@ func New(debug bool) PingvinKL {
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
}

View File

@ -1,4 +1,4 @@
package pingvinKL
package pingvin
import (
"fmt"