Compare commits
25 Commits
Author | SHA1 | Date |
---|---|---|
Jarno Rankinen | 4045de3f15 | |
Jarno Rankinen | d5e87bd073 | |
Jarno Rankinen | 26c16e7c8e | |
Jarno Rankinen | 972398d8ea | |
Jarno Rankinen | 8c825580fa | |
Jarno Rankinen | 6431315462 | |
Jarno Rankinen | 11debcecc5 | |
Jarno Rankinen | 2c00babf4d | |
Jarno Rankinen | cf22dcf6f9 | |
Jarno Rankinen | 86afcef6ec | |
Jarno Rankinen | 25887e1111 | |
Jarno Rankinen | ef6627dae8 | |
Jarno Rankinen | d1f734dd32 | |
Jarno Rankinen | 8278bc9445 | |
Jarno Rankinen | 28a555fa2b | |
Jarno Rankinen | ea8ca1a6df | |
Jarno Rankinen | d9fbbfac1c | |
Jarno Rankinen | 5458c3ba86 | |
Jarno Rankinen | 027d678a09 | |
Jarno Rankinen | 32fa6d4321 | |
Jarno Rankinen | 0aeb54dbd8 | |
Jarno Rankinen | 1a4b22df02 | |
Jarno Rankinen | 19e047b142 | |
Jarno Rankinen | efbfe72bba | |
Jarno Rankinen | af4405550c |
|
@ -1,9 +1,9 @@
|
||||||
name: Add issues to project
|
name: Add issues to project
|
||||||
|
|
||||||
on:
|
#on:
|
||||||
issues:
|
# issues:
|
||||||
types:
|
# types:
|
||||||
- opened
|
# - opened
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
add-to-project:
|
add-to-project:
|
||||||
|
|
|
@ -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 }}
|
|
@ -1,3 +1,4 @@
|
||||||
.vscode/
|
.vscode/
|
||||||
build.sh
|
.idea/
|
||||||
BUILD/*
|
BUILD/*
|
||||||
|
TMP/*
|
||||||
|
|
|
@ -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:"
|
52
README.md
52
README.md
|
@ -23,10 +23,6 @@ has been used.
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
- clone or download the repo
|
- clone or download the repo
|
||||||
- `static/html/index.html` is symlinked to `coils` and `registers`
|
|
||||||
for development purposes, the symlinks need to be dereferenced before
|
|
||||||
building the binary on filesystems that support symlinks
|
|
||||||
- Replace symlinks with copies of the files or use e.g. `tar -h`
|
|
||||||
- Build for the correct architecture, e.g. for Linux 32-bit ARM (Rpi Zero W 1):
|
- Build for the correct architecture, e.g. for Linux 32-bit ARM (Rpi Zero W 1):
|
||||||
```
|
```
|
||||||
cd /path/to/repo
|
cd /path/to/repo
|
||||||
|
@ -37,30 +33,34 @@ has been used.
|
||||||
- CLI flags:
|
- CLI flags:
|
||||||
```
|
```
|
||||||
-cert string
|
-cert string
|
||||||
Path to SSL public key to use for HTTPS (default "~/.config/enervent-ctrl/certificate.pem")
|
Path to SSL public key to use for HTTPS (default "~/.config/enervent-ctrl/certificate.pem")
|
||||||
-debug
|
-debug
|
||||||
Enable debug logging
|
Enable debug logging
|
||||||
|
-disable-auth
|
||||||
|
Disable HTTP basic authentication (default true)
|
||||||
-enable-metrics
|
-enable-metrics
|
||||||
Enable the built-in Prometheus exporter (default true)
|
Enable the built-in Prometheus exporter (default true)
|
||||||
-httplog
|
-httplog
|
||||||
Enable HTTP access logging
|
Enable HTTP access logging
|
||||||
-interval int
|
-interval int
|
||||||
Set the interval of background updates (default 4)
|
Set the interval of background updates (default 4)
|
||||||
-key string
|
-key string
|
||||||
Path to SSL private key to use for HTTPS (default "~/.config/enervent-ctrl/privatekey.pem")
|
Path to SSL private key to use for HTTPS (default "~/.config/enervent-ctrl/privatekey.pem")
|
||||||
-logfile string
|
-logfile string
|
||||||
Path to log file. Default is empty string, log to stdout
|
Path to log file. Default is empty string, log to stdout
|
||||||
-password string
|
-password string
|
||||||
Password for HTTP Basic Authentication (default "enervent")
|
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
|
-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.
|
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
|
-serial string
|
||||||
Path to serial console for RS-485 connection. Defaults to /dev/ttyS0 (default "/dev/ttyS0")
|
Path to serial console for RS-485 connection. Defaults to /dev/ttyS0 (default "/dev/ttyS0")
|
||||||
-username string
|
-username string
|
||||||
Username for HTTP Basic Authentication (default "pingvin")
|
Username for HTTP Basic Authentication (default "pingvin")
|
||||||
```
|
```
|
||||||
On first run, the daemon generates `~/.config/enervent-ctrl/configuration.yaml` with default values.
|
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 precedenence over the config file.
|
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
|
- `serial_address:` Path to RS-485 serial device
|
||||||
- `port:` TCP port for the REST API to listen on
|
- `port:` TCP port for the REST API to listen on
|
||||||
- `ssl_certificate:` Path to SSL certificate for HTTPS
|
- `ssl_certificate:` Path to SSL certificate for HTTPS
|
||||||
|
@ -110,10 +110,13 @@ WantedBy=default.target
|
||||||
### Connecting to the Pingvin unit
|
### Connecting to the Pingvin unit
|
||||||
#### RPi/computer running the daemon
|
#### RPi/computer running the daemon
|
||||||
- Connect an RS-485 adapter to the computer you intend to run the daemon on
|
- Connect an RS-485 adapter to the computer you intend to run the daemon on
|
||||||
- Tested only on a RPi 4B and Zero W 1 with the Zihatec RS-485 HAT
|
- Tested on:
|
||||||
- You may need terminating resistors in your adapter, see documentation of your adapter.
|
- 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.
|
- Ensure the user you intend to run the daemon as has read/write privileges to the serial device.
|
||||||
- No need to run the daemon as root, and it is not recommended
|
- **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
|
#### Pingvin
|
||||||
- Shut down the main power of the unit
|
- Shut down the main power of the unit
|
||||||
|
@ -140,6 +143,14 @@ should be enough length in the wires to move the lid with the switch connected o
|
||||||
|
|
||||||
- There are so many variations for HASS configs, that definite instructions are hard to do.
|
- 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)
|
- 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
|
- 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)
|
- Restart Home Assistant (A full reload doesn't seem to be enough for all REST integration features to update)
|
||||||
|
|
||||||
|
@ -148,3 +159,6 @@ Work is part of my Bachelor's Thesis at Oulu University
|
||||||
of Applied Sciences.
|
of Applied Sciences.
|
||||||
|
|
||||||
Pingvin and Kotilämpö are registered trademarks of Enervent Zehnder Oy.
|
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>
|
||||||
|
|
|
@ -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" .
|
28
go.mod
28
go.mod
|
@ -1,27 +1,25 @@
|
||||||
module github.com/0ranki/enervent-ctrl
|
module github.com/0ranki/enervent-ctrl
|
||||||
|
|
||||||
go 1.18
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433
|
github.com/0ranki/https-go v0.0.0-20230314073101-4eca22af948c
|
||||||
github.com/goburrow/modbus v0.1.0
|
github.com/goburrow/modbus v0.1.0
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.2
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.19.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/goburrow/serial v0.1.0 // indirect
|
github.com/goburrow/serial v0.1.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/prometheus/client_model v0.6.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/common v0.50.0 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/procfs v0.13.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
37
go.sum
37
go.sum
|
@ -31,8 +31,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
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=
|
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=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433 h1:QT2IRJnhIdCSr26LJktnZnBpHdiLfTrUFzLSdP3h9Wo=
|
github.com/0ranki/https-go v0.0.0-20230314073101-4eca22af948c h1:Tmui5U+C7KF4gYHnpXxe2sfROcrGksSmFheTVJAHdLo=
|
||||||
github.com/0ranki/https-go v0.0.0-20230314064508-ba9a558db433/go.mod h1:r4Jb05+PuiVKHDYwSsSBuSz4LpOlC2DgOY4N58+K8Hk=
|
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/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/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-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
@ -48,6 +48,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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/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/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/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
@ -62,6 +64,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
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 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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 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-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-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
@ -107,6 +111,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
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 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
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 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/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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
@ -119,6 +125,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||||
github.com/google/go-cmp v0.5.4/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.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 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/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 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/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
@ -134,6 +142,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
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 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
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.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/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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
@ -152,11 +162,14 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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 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.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-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
@ -175,18 +188,24 @@ github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqr
|
||||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
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 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
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-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-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.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.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 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
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.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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
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.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 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
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.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.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.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
|
@ -194,6 +213,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
||||||
github.com/prometheus/procfs v0.7.3/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 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
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/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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
@ -334,6 +355,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/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 h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
@ -389,9 +412,10 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-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-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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
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.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.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.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
@ -467,13 +491,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
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/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 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-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 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-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/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.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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,103 +1,323 @@
|
||||||
automation:
|
## FAN / HEATER CONTROL AUTOMATIONS
|
||||||
## Max heating
|
## THESE WILL AFFECT THE BEHAVIOUR OF THE UNIT
|
||||||
- alias: Penguin Max Heating input
|
- alias: Penguin Auto Heater Disable v0.2.0
|
||||||
description: ""
|
description: |-
|
||||||
trigger:
|
Temperature is 0.5 over setpoint and outside temperature over 10
|
||||||
- platform: state
|
-> heater off
|
||||||
entity_id:
|
trigger:
|
||||||
- input_boolean.penguin_max_heating
|
- platform: state
|
||||||
condition: []
|
entity_id:
|
||||||
action:
|
- sensor.penguin_temperature_delta
|
||||||
- if:
|
for:
|
||||||
- condition: state
|
hours: 0
|
||||||
entity_id: input_boolean.penguin_max_heating
|
minutes: 0
|
||||||
state: "on"
|
seconds: 0
|
||||||
- condition: state
|
- platform: numeric_state
|
||||||
entity_id: binary_sensor.penguin_max_heating
|
entity_id: sensor.penguin_temperature_delta
|
||||||
state: "off"
|
above: 0.5
|
||||||
- condition: numeric_state
|
condition:
|
||||||
entity_id: sensor.penguin_room_temperature_1
|
- condition: numeric_state
|
||||||
below: input_number.penguin_temperature_setting_helper
|
entity_id: sensor.penguin_intake_air
|
||||||
then:
|
above: 10
|
||||||
- service: rest_command.penguin_max_heating_on
|
value_template: " {{ states['sensor.penguin_intake_air'].state }}"
|
||||||
data: {}
|
- condition: state
|
||||||
else: []
|
entity_id: sun.sun
|
||||||
- if:
|
state: above_horizon
|
||||||
- condition: state
|
action:
|
||||||
entity_id: input_boolean.penguin_max_heating
|
- delay:
|
||||||
state: "off"
|
hours: 0
|
||||||
then:
|
minutes: 15
|
||||||
- service: rest_command.penguin_max_heating_off
|
seconds: 0
|
||||||
data: {}
|
milliseconds: 0
|
||||||
- if:
|
- condition: numeric_state
|
||||||
- condition: numeric_state
|
entity_id: sensor.penguin_intake_air
|
||||||
entity_id: sensor.penguin_room_temperature_1
|
above: 10
|
||||||
above: input_number.penguin_temperature_setting_helper
|
value_template: "{{ states['sensor.penguin_intake_air'].state }}"
|
||||||
- condition: state
|
- service: rest_command.penguin_heater_disable
|
||||||
entity_id: input_boolean.penguin_max_heating
|
data: { }
|
||||||
state: "on"
|
- service: input_boolean.turn_off
|
||||||
then:
|
data: { }
|
||||||
- service: input_boolean.turn_off
|
target:
|
||||||
data: {}
|
entity_id: input_boolean.penguin_after_heater
|
||||||
target:
|
- delay:
|
||||||
entity_id: input_boolean.penguin_max_heating
|
hours: 0
|
||||||
mode: single
|
minutes: 0
|
||||||
- alias: Penguin Max Heating sensor
|
seconds: 5
|
||||||
description: ""
|
milliseconds: 0
|
||||||
trigger:
|
- service: rest_command.penguin_circulation_adaptive
|
||||||
- platform: state
|
data: { }
|
||||||
entity_id:
|
mode: single
|
||||||
- binary_sensor.penguin_max_heating
|
- alias: Penguin Auto Heater Enable v0.2.0
|
||||||
condition: []
|
description: |-
|
||||||
action:
|
Temperature below setpoint
|
||||||
- if:
|
-> heater on
|
||||||
- condition: state
|
trigger:
|
||||||
entity_id: binary_sensor.penguin_max_heating
|
- platform: numeric_state
|
||||||
state: "on"
|
entity_id: sensor.penguin_temperature_delta
|
||||||
then:
|
value_template: "{{ states['sensor.penguin_temperature_delta'].state }}"
|
||||||
- service: input_boolean.turn_on
|
below: 0
|
||||||
data: {}
|
- platform: sun
|
||||||
target:
|
event: sunset
|
||||||
entity_id: input_boolean.penguin_max_heating
|
offset: 0
|
||||||
else:
|
enabled: true
|
||||||
- service: input_boolean.turn_off
|
condition: [ ]
|
||||||
data: {}
|
action:
|
||||||
target:
|
- service: rest_command.penguin_heater_enable
|
||||||
entity_id: input_boolean.penguin_max_heating
|
data: { }
|
||||||
mode: single
|
- 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
|
||||||
|
|
||||||
|
## DASHBOARD TOGGLE/SETTING RELATED AUTOMATIONS
|
||||||
## Max cooling
|
## REQUIRED FOR e.g. SETTING THE TEMPERATURE SETPOINT VIA HA AND OTHER ACTIONS
|
||||||
- alias: Penguin max cooling sensor
|
- alias: Penguin After Heater Input v0.2.0
|
||||||
description: ""
|
description: "Actions to take when input_boolean.penguin_after_heater is toggled"
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id:
|
entity_id:
|
||||||
- binary_sensor.penguin_max_cooling
|
- input_boolean.penguin_after_heater
|
||||||
condition: []
|
condition: [ ]
|
||||||
action:
|
action:
|
||||||
- if:
|
- if:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: binary_sensor.penguin_max_cooling
|
entity_id: input_boolean.penguin_after_heater
|
||||||
state: "on"
|
state: "on"
|
||||||
then:
|
- condition: state
|
||||||
- service: input_boolean.turn_on
|
entity_id: binary_sensor.penguin_after_heater_enabled
|
||||||
data: {}
|
state: "off"
|
||||||
target:
|
then:
|
||||||
entity_id: input_boolean.penguin_max_cooling
|
- service: rest_command.penguin_heater_enable
|
||||||
else:
|
data: { }
|
||||||
- service: input_boolean.turn_off
|
- if:
|
||||||
data: {}
|
- condition: state
|
||||||
target:
|
entity_id: input_boolean.penguin_after_heater
|
||||||
entity_id: input_boolean.penguin_max_cooling
|
state: "off"
|
||||||
mode: single
|
- condition: state
|
||||||
- alias: Penguin max cooling input
|
entity_id: binary_sensor.penguin_after_heater_enabled
|
||||||
|
state: "on"
|
||||||
|
then:
|
||||||
|
- service: rest_command.penguin_heater_disable
|
||||||
|
data: { }
|
||||||
|
mode: single
|
||||||
|
- alias: Penguin boost input v0.2.0
|
||||||
|
description: "Actions when toggling input_boolean.penguin_boost"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id:
|
||||||
|
- input_boolean.penguin_boost
|
||||||
|
condition: [ ]
|
||||||
|
action:
|
||||||
|
- if:
|
||||||
|
- condition: state
|
||||||
|
entity_id: input_boolean.penguin_boost
|
||||||
|
state: "on"
|
||||||
|
then:
|
||||||
|
- service: rest_command.penguin_boost_on
|
||||||
|
data: { }
|
||||||
|
else:
|
||||||
|
- service: rest_command.penguin_boost_off
|
||||||
|
data: { }
|
||||||
|
- delay:
|
||||||
|
hours: 0
|
||||||
|
minutes: 0
|
||||||
|
seconds: 5
|
||||||
|
milliseconds: 0
|
||||||
|
- if:
|
||||||
|
- condition: state
|
||||||
|
entity_id: binary_sensor.penguin_boost
|
||||||
|
state: "on"
|
||||||
|
then:
|
||||||
|
- service: input_boolean.turn_on
|
||||||
|
data: { }
|
||||||
|
target:
|
||||||
|
entity_id: input_boolean.penguin_boost
|
||||||
|
else:
|
||||||
|
- service: input_boolean.turn_off
|
||||||
|
data: { }
|
||||||
|
target:
|
||||||
|
entity_id: input_boolean.penguin_boost
|
||||||
|
mode: single
|
||||||
|
- alias: Penguin circulation fan mode sensor v0.2.0
|
||||||
|
description: ""
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id:
|
||||||
|
- binary_sensor.penguin_circulation_adaptive
|
||||||
|
condition: [ ]
|
||||||
|
action:
|
||||||
|
- if:
|
||||||
|
- condition: state
|
||||||
|
entity_id: binary_sensor.penguin_circulation_adaptive
|
||||||
|
state: "on"
|
||||||
|
then:
|
||||||
|
- service: input_boolean.turn_on
|
||||||
|
data: { }
|
||||||
|
target:
|
||||||
|
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||||
|
else:
|
||||||
|
- service: input_boolean.turn_off
|
||||||
|
data: { }
|
||||||
|
target:
|
||||||
|
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||||
|
- delay:
|
||||||
|
hours: 0
|
||||||
|
minutes: 0
|
||||||
|
seconds: 2
|
||||||
|
milliseconds: 0
|
||||||
|
- service: homeassistant.update_entity
|
||||||
|
data: { }
|
||||||
|
target:
|
||||||
|
entity_id: sensor.penguin_circulation_fan_pct
|
||||||
|
- service: homeassistant.update_entity
|
||||||
|
data: { }
|
||||||
|
target:
|
||||||
|
entity_id: sensor.penguin_operating_mode
|
||||||
|
mode: single
|
||||||
|
- alias: Penguin circulation fan mode v0.2.0
|
||||||
|
description: ""
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id:
|
||||||
|
- input_boolean.penguin_circulation_fan_adaptive
|
||||||
|
condition: [ ]
|
||||||
|
action:
|
||||||
|
- if:
|
||||||
|
- condition: state
|
||||||
|
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
||||||
|
state: "on"
|
||||||
|
then:
|
||||||
|
- service: rest_command.penguin_circulation_adaptive
|
||||||
|
data: { }
|
||||||
|
else:
|
||||||
|
- service: rest_command.penguin_circulation_manual
|
||||||
|
data: { }
|
||||||
|
mode: single
|
||||||
|
- alias: Penguin max cooling input v0.2.0
|
||||||
description: ""
|
description: ""
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id:
|
entity_id:
|
||||||
- input_boolean.penguin_max_cooling
|
- input_boolean.penguin_max_cooling
|
||||||
condition: []
|
condition: [ ]
|
||||||
action:
|
action:
|
||||||
- if:
|
- if:
|
||||||
- condition: state
|
- condition: state
|
||||||
|
@ -111,15 +331,15 @@ automation:
|
||||||
above: input_number.penguin_temperature_setting_helper
|
above: input_number.penguin_temperature_setting_helper
|
||||||
then:
|
then:
|
||||||
- service: rest_command.penguin_max_cooling_on
|
- service: rest_command.penguin_max_cooling_on
|
||||||
data: {}
|
data: { }
|
||||||
else: []
|
else: [ ]
|
||||||
- if:
|
- if:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: input_boolean.penguin_max_cooling
|
entity_id: input_boolean.penguin_max_cooling
|
||||||
state: "off"
|
state: "off"
|
||||||
then:
|
then:
|
||||||
- service: rest_command.penguin_max_cooling_off
|
- service: rest_command.penguin_max_cooling_off
|
||||||
data: {}
|
data: { }
|
||||||
- if:
|
- if:
|
||||||
- condition: numeric_state
|
- condition: numeric_state
|
||||||
entity_id: sensor.penguin_room_temperature_1
|
entity_id: sensor.penguin_room_temperature_1
|
||||||
|
@ -129,88 +349,220 @@ automation:
|
||||||
state: "on"
|
state: "on"
|
||||||
then:
|
then:
|
||||||
- service: input_boolean.turn_off
|
- service: input_boolean.turn_off
|
||||||
data: {}
|
data: { }
|
||||||
target:
|
target:
|
||||||
entity_id: input_boolean.penguin_max_cooling
|
entity_id: input_boolean.penguin_max_cooling
|
||||||
mode: single
|
mode: single
|
||||||
|
- alias: Penguin max cooling sensor v0.2.0
|
||||||
|
|
||||||
## Circulation fan mode
|
|
||||||
- alias: Penguin circulation fan mode
|
|
||||||
description: ""
|
|
||||||
trigger:
|
|
||||||
- platform: state
|
|
||||||
entity_id:
|
|
||||||
- input_boolean.penguin_circulation_fan_adaptive
|
|
||||||
condition: []
|
|
||||||
action:
|
|
||||||
- if:
|
|
||||||
- condition: state
|
|
||||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
|
||||||
state: "on"
|
|
||||||
then:
|
|
||||||
- service: rest_command.penguin_circulation_adaptive
|
|
||||||
data: {}
|
|
||||||
- if:
|
|
||||||
- condition: state
|
|
||||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
|
||||||
state: "off"
|
|
||||||
then:
|
|
||||||
- service: rest_command.penguin_circulation_manual
|
|
||||||
data: {}
|
|
||||||
mode: single
|
|
||||||
- alias: Penguin circulation fan mode sensor
|
|
||||||
description: ""
|
description: ""
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id:
|
entity_id:
|
||||||
- binary_sensor.penguin_circulation_adaptive
|
- binary_sensor.penguin_max_cooling
|
||||||
condition: []
|
condition: [ ]
|
||||||
action:
|
action:
|
||||||
- if:
|
- if:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: binary_sensor.penguin_circulation_adaptive
|
entity_id: binary_sensor.penguin_max_cooling
|
||||||
state: "on"
|
state: "on"
|
||||||
then:
|
then:
|
||||||
- service: input_boolean.turn_on
|
- service: input_boolean.turn_on
|
||||||
data: {}
|
data: { }
|
||||||
target:
|
target:
|
||||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
entity_id: input_boolean.penguin_max_cooling
|
||||||
else:
|
else:
|
||||||
- service: input_boolean.turn_off
|
- service: input_boolean.turn_off
|
||||||
data: {}
|
data: { }
|
||||||
target:
|
target:
|
||||||
entity_id: input_boolean.penguin_circulation_fan_adaptive
|
entity_id: input_boolean.penguin_max_cooling
|
||||||
mode: single
|
mode: single
|
||||||
|
- alias: Penguin Max Heating input v0.2.0
|
||||||
|
description: ""
|
||||||
## Target temperature setting automations
|
trigger:
|
||||||
- alias: Penguin temperature down
|
- 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: ""
|
description: ""
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id:
|
entity_id:
|
||||||
- input_button.penguin_temperature_down
|
- input_button.penguin_temperature_down
|
||||||
condition: []
|
condition: [ ]
|
||||||
action:
|
action:
|
||||||
- service: rest_command.penguin_temperature_down
|
|
||||||
data: {}
|
|
||||||
- service: input_number.decrement
|
- service: input_number.decrement
|
||||||
data: {}
|
data: { }
|
||||||
target:
|
target:
|
||||||
entity_id: input_number.penguin_temperature_setting_helper
|
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
|
- service: homeassistant.update_entity
|
||||||
data: {}
|
data: { }
|
||||||
target:
|
target:
|
||||||
entity_id: sensor.penguin_temperature_setting
|
entity_id: sensor.penguin_temperature_setting
|
||||||
mode: single
|
mode: restart
|
||||||
- alias: Penguin Temperature setting updater
|
- 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: ""
|
description: ""
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id:
|
entity_id:
|
||||||
- sensor.penguin_temperature_setting
|
- sensor.penguin_temperature_setting
|
||||||
condition: []
|
condition: [ ]
|
||||||
action:
|
action:
|
||||||
- service: input_number.set_value
|
- service: input_number.set_value
|
||||||
data:
|
data:
|
||||||
|
@ -218,22 +570,3 @@ automation:
|
||||||
target:
|
target:
|
||||||
entity_id: input_number.penguin_temperature_setting_helper
|
entity_id: input_number.penguin_temperature_setting_helper
|
||||||
mode: single
|
mode: single
|
||||||
- alias: Penguin temperature up
|
|
||||||
description: ""
|
|
||||||
trigger:
|
|
||||||
- platform: state
|
|
||||||
entity_id:
|
|
||||||
- input_button.penguin_temperature_up
|
|
||||||
condition: []
|
|
||||||
action:
|
|
||||||
- service: rest_command.penguin_temperature_up
|
|
||||||
data: {}
|
|
||||||
- service: input_number.increment
|
|
||||||
data: {}
|
|
||||||
target:
|
|
||||||
entity_id: input_number.penguin_temperature_setting_helper
|
|
||||||
- service: homeassistant.update_entity
|
|
||||||
data: {}
|
|
||||||
target:
|
|
||||||
entity_id: sensor.penguin_temperature_setting
|
|
||||||
mode: single
|
|
|
@ -1,5 +1,6 @@
|
||||||
views:
|
views:
|
||||||
- title: Penguin
|
- title: Pingvin
|
||||||
|
icon: mdi:penguin
|
||||||
cards:
|
cards:
|
||||||
- type: vertical-stack
|
- type: vertical-stack
|
||||||
cards:
|
cards:
|
||||||
|
@ -24,6 +25,11 @@ views:
|
||||||
- entity: input_boolean.penguin_after_heater
|
- entity: input_boolean.penguin_after_heater
|
||||||
name: Heating allowed
|
name: Heating allowed
|
||||||
secondary_info: last-changed
|
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
|
state_color: true
|
||||||
- square: false
|
- square: false
|
||||||
columns: 4
|
columns: 4
|
||||||
|
@ -122,8 +128,6 @@ views:
|
||||||
name: Outside air 24h avg
|
name: Outside air 24h avg
|
||||||
- entity: sensor.penguin_supply_air_hrc
|
- entity: sensor.penguin_supply_air_hrc
|
||||||
name: Intake after HRC
|
name: Intake after HRC
|
||||||
- entity: sensor.penguin_supply_air_humidity
|
|
||||||
name: Intake humidity
|
|
||||||
- entity: sensor.penguin_supply_air
|
- entity: sensor.penguin_supply_air
|
||||||
name: Supply air
|
name: Supply air
|
||||||
- entity: sensor.penguin_return_water
|
- entity: sensor.penguin_return_water
|
||||||
|
@ -141,5 +145,9 @@ views:
|
||||||
name: HRC Efficiency intake
|
name: HRC Efficiency intake
|
||||||
- entity: sensor.penguin_hrc_efficiency_extract
|
- 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: Measurements
|
||||||
title: Heating & Ventilation
|
title: Heating & Ventilation
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
views:
|
views:
|
||||||
- title: Penguin
|
- title: Pingvin
|
||||||
|
icon: mdi:penguin
|
||||||
cards:
|
cards:
|
||||||
- type: vertical-stack
|
- type: vertical-stack
|
||||||
cards:
|
cards:
|
||||||
|
@ -24,6 +25,11 @@ views:
|
||||||
- entity: input_boolean.penguin_after_heater
|
- entity: input_boolean.penguin_after_heater
|
||||||
name: Lämmitys sallittu
|
name: Lämmitys sallittu
|
||||||
secondary_info: last-changed
|
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
|
state_color: true
|
||||||
- square: false
|
- square: false
|
||||||
columns: 4
|
columns: 4
|
||||||
|
@ -122,8 +128,6 @@ views:
|
||||||
name: Ulkoilma 24h keskiarvo
|
name: Ulkoilma 24h keskiarvo
|
||||||
- entity: sensor.penguin_supply_air_hrc
|
- entity: sensor.penguin_supply_air_hrc
|
||||||
name: Tuloilma LTO jälkeen
|
name: Tuloilma LTO jälkeen
|
||||||
- entity: sensor.penguin_supply_air_humidity
|
|
||||||
name: Tuloilma kosteus
|
|
||||||
- entity: sensor.penguin_supply_air
|
- entity: sensor.penguin_supply_air
|
||||||
name: Tuloilma
|
name: Tuloilma
|
||||||
- entity: sensor.penguin_return_water
|
- entity: sensor.penguin_return_water
|
||||||
|
@ -141,5 +145,9 @@ views:
|
||||||
name: LTO hyötysuhde tuloilma
|
name: LTO hyötysuhde tuloilma
|
||||||
- entity: sensor.penguin_hrc_efficiency_extract
|
- entity: sensor.penguin_hrc_efficiency_extract
|
||||||
name: LTO hyötysuhde poistoilma
|
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: Mittaukset
|
||||||
title: Lämmitys & IV
|
title: Lämmitys & IV
|
||||||
|
|
|
@ -9,7 +9,7 @@ input_boolean:
|
||||||
name: Penguin Overpressure
|
name: Penguin Overpressure
|
||||||
icon: mdi:fireplace
|
icon: mdi:fireplace
|
||||||
penguin_boost:
|
penguin_boost:
|
||||||
name: Penguin Overpressure
|
name: Penguin Boost
|
||||||
icon: mdi:fan-plus
|
icon: mdi:fan-plus
|
||||||
penguin_max_cooling:
|
penguin_max_cooling:
|
||||||
name: Penguin Max Cooling
|
name: Penguin Max Cooling
|
||||||
|
@ -17,6 +17,12 @@ input_boolean:
|
||||||
penguin_after_heater:
|
penguin_after_heater:
|
||||||
name: Penguin After Heater
|
name: Penguin After Heater
|
||||||
icon: mdi:heating-coil
|
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:
|
input_button:
|
||||||
penguin_temperature_up:
|
penguin_temperature_up:
|
||||||
name: Penguin temperature up
|
name: Penguin temperature up
|
||||||
|
@ -30,3 +36,4 @@ input_number:
|
||||||
min: 20
|
min: 20
|
||||||
max: 30
|
max: 30
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
|
step: 0.5
|
|
@ -28,6 +28,14 @@ rest:
|
||||||
value_template: "{{ value_json['fan_pct'] }}"
|
value_template: "{{ value_json['fan_pct'] }}"
|
||||||
unit_of_measurement: "%"
|
unit_of_measurement: "%"
|
||||||
icon: mdi:fan
|
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"
|
- name: "Penguin HRC efficiency intake"
|
||||||
value_template: "{{ value_json['hrc_efficiency_in'] }}"
|
value_template: "{{ value_json['hrc_efficiency_in'] }}"
|
||||||
unit_of_measurement: "%"
|
unit_of_measurement: "%"
|
||||||
|
@ -36,10 +44,6 @@ rest:
|
||||||
value_template: "{{ value_json['hrc_efficiency_ex'] }}"
|
value_template: "{{ value_json['hrc_efficiency_ex'] }}"
|
||||||
unit_of_measurement: "%"
|
unit_of_measurement: "%"
|
||||||
icon: mdi:recycle
|
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"
|
- name: "Penguin supply air"
|
||||||
value_template: "{{ value_json['measurements']['supply_heated'] }}"
|
value_template: "{{ value_json['measurements']['supply_heated'] }}"
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
|
@ -99,6 +103,9 @@ rest:
|
||||||
- name: "Penguin after heater enabled"
|
- name: "Penguin after heater enabled"
|
||||||
value_template: "{{ value_json['coils'][54]['value'] }}"
|
value_template: "{{ value_json['coils'][54]['value'] }}"
|
||||||
icon: mdi:heating-coil
|
icon: mdi:heating-coil
|
||||||
|
- name: "Penguin summer night cooling enabled"
|
||||||
|
value_template: "{{ value_json['coils'][12]['value'] }}"
|
||||||
|
icon: mdi:heating-coil
|
||||||
|
|
||||||
template:
|
template:
|
||||||
- sensor:
|
- sensor:
|
||||||
|
@ -125,13 +132,13 @@ rest_command:
|
||||||
username: pingvin
|
username: pingvin
|
||||||
password: enervent
|
password: enervent
|
||||||
penguin_boost_on:
|
penguin_boost_on:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/10/1
|
url: https://IP_ADDRESS:8888/api/v1/coils/10/1
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
username: pingvin
|
username: pingvin
|
||||||
password: enervent
|
password: enervent
|
||||||
penguin_boost_off:
|
penguin_boost_off:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/10/0
|
url: https://IP_ADDRESS:8888/api/v1/coils/10/0
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
username: pingvin
|
username: pingvin
|
||||||
|
@ -158,37 +165,37 @@ rest_command:
|
||||||
username: pingvin
|
username: pingvin
|
||||||
password: enervent
|
password: enervent
|
||||||
penguin_max_cooling_on:
|
penguin_max_cooling_on:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/7/1
|
url: https://IP_ADDRESS:8888/api/v1/coils/7/1
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
username: pingvin
|
username: pingvin
|
||||||
password: enervent
|
password: enervent
|
||||||
penguin_max_cooling_off:
|
penguin_max_cooling_off:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/7/0
|
url: https://IP_ADDRESS:8888/api/v1/coils/7/0
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
username: pingvin
|
username: pingvin
|
||||||
password: enervent
|
password: enervent
|
||||||
penguin_temperature_up:
|
penguin_temperature_up:
|
||||||
url: https://192.168.0.210:8888/api/v1/temperature/up
|
url: https://IP_ADDRESS:8888/api/v1/temperature/up
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
username: pingvin
|
username: pingvin
|
||||||
password: enervent
|
password: enervent
|
||||||
penguin_temperature_down:
|
penguin_temperature_down:
|
||||||
url: https://192.168.0.210:8888/api/v1/temperature/down
|
url: https://IP_ADDRESS:8888/api/v1/temperature/down
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
username: pingvin
|
username: pingvin
|
||||||
password: enervent
|
password: enervent
|
||||||
penguin_heater_enabled:
|
penguin_heater_enabled:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/54/1
|
url: https://IP_ADDRESS:8888/api/v1/coils/54/1
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
username: pingvin
|
username: pingvin
|
||||||
password: enervent
|
password: enervent
|
||||||
penguin_heater_disabled:
|
penguin_heater_disabled:
|
||||||
url: https://192.168.0.210:8888/api/v1/coils/54/0
|
url: https://IP_ADDRESS:8888/api/v1/coils/54/0
|
||||||
method: POST
|
method: POST
|
||||||
verify_ssl: false
|
verify_ssl: false
|
||||||
username: pingvin
|
username: pingvin
|
||||||
|
|
188
main.go
188
main.go
|
@ -2,17 +2,12 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/0ranki/enervent-ctrl/pingvin"
|
"github.com/0ranki/enervent-ctrl/pingvin"
|
||||||
|
@ -30,7 +25,7 @@ import (
|
||||||
var static embed.FS
|
var static embed.FS
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "0.0.26"
|
version = "0.2.0"
|
||||||
device pingvin.Pingvin
|
device pingvin.Pingvin
|
||||||
config Conf
|
config Conf
|
||||||
usernamehash [32]byte
|
usernamehash [32]byte
|
||||||
|
@ -42,6 +37,7 @@ type Conf struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
SslCertificate string `yaml:"ssl_certificate"`
|
SslCertificate string `yaml:"ssl_certificate"`
|
||||||
SslPrivatekey string `yaml:"ssl_privatekey"`
|
SslPrivatekey string `yaml:"ssl_privatekey"`
|
||||||
|
DisableAuth bool `yaml:"disable_auth"`
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
Interval int `yaml:"interval"`
|
Interval int `yaml:"interval"`
|
||||||
|
@ -49,149 +45,7 @@ type Conf struct {
|
||||||
LogFile string `yaml:"log_file"`
|
LogFile string `yaml:"log_file"`
|
||||||
LogAccess bool `yaml:"log_access"`
|
LogAccess bool `yaml:"log_access"`
|
||||||
Debug bool `yaml:"debug"`
|
Debug bool `yaml:"debug"`
|
||||||
}
|
ReadOnly bool `yaml:"read_only"`
|
||||||
|
|
||||||
// HTTP Basic Authentication middleware for http.HandlerFunc
|
|
||||||
func authHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
// Based on https://www.alexedwards.net/blog/basic-authentication-in-go
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user, pass, ok := r.BasicAuth()
|
|
||||||
if ok {
|
|
||||||
userHash := sha256.Sum256([]byte(user))
|
|
||||||
passHash := sha256.Sum256([]byte(pass))
|
|
||||||
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
|
|
||||||
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
|
|
||||||
if usernameMatch && passwordMatch {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(user) == 0 {
|
|
||||||
user = "-"
|
|
||||||
}
|
|
||||||
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
|
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP Basic Authentication middleware for http.Handler
|
|
||||||
func authHandler(next http.Handler) http.HandlerFunc {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user, pass, ok := r.BasicAuth()
|
|
||||||
if ok {
|
|
||||||
userHash := sha256.Sum256([]byte(user))
|
|
||||||
passHash := sha256.Sum256([]byte(pass))
|
|
||||||
usernameMatch := (subtle.ConstantTimeCompare(userHash[:], usernamehash[:]) == 1)
|
|
||||||
passwordMatch := (subtle.ConstantTimeCompare(passHash[:], passwordhash[:]) == 1)
|
|
||||||
if usernameMatch && passwordMatch {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(user) == 0 {
|
|
||||||
user = "-"
|
|
||||||
}
|
|
||||||
log.Println("Authentication failed: IP:", r.RemoteAddr, "URI:", r.RequestURI, "username:", user)
|
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// \/api/v1/coils endpoint
|
|
||||||
func coils(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/")
|
|
||||||
if len(pathparams[0]) == 0 {
|
|
||||||
json.NewEncoder(w).Encode(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
|
|
||||||
}
|
|
||||||
device.ReadCoil(uint16(intaddr))
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
device.ReadRegister(uint16(intaddr))
|
|
||||||
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
|
|
||||||
}
|
|
||||||
_, 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 {
|
|
||||||
device.Temperature(pathparams[0])
|
|
||||||
json.NewEncoder(w).Encode(device.Registers[135])
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the HTTP server
|
// Start the HTTP server
|
||||||
|
@ -210,6 +64,8 @@ func serve(cert, key *string) {
|
||||||
}
|
}
|
||||||
htmlroot := http.FileServer(http.FS(html))
|
htmlroot := http.FileServer(http.FS(html))
|
||||||
http.HandleFunc("/", authHandler(htmlroot))
|
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)
|
logdst, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModeAppend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -259,12 +115,12 @@ func parseConfigFile() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conffile := confpath + "/configuration.yaml"
|
conffile := confpath + "/configuration.yaml"
|
||||||
yamldata, err := ioutil.ReadFile(conffile)
|
yamldata, err := os.ReadFile(conffile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Configuration file", conffile, "not found")
|
log.Println("Configuration file", conffile, "not found")
|
||||||
log.Println("Generating", conffile, "with default values")
|
log.Println("Generating", conffile, "with default values")
|
||||||
initDefaultConfig(confpath)
|
initDefaultConfig(confpath)
|
||||||
if yamldata, err = ioutil.ReadFile(conffile); err != nil {
|
if yamldata, err = os.ReadFile(conffile); err != nil {
|
||||||
log.Fatal("Error parsing configuration:", err)
|
log.Fatal("Error parsing configuration:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,17 +133,19 @@ func parseConfigFile() {
|
||||||
// Write the default configuration to $HOME/.config/enervent-ctrl/configuration.yaml
|
// Write the default configuration to $HOME/.config/enervent-ctrl/configuration.yaml
|
||||||
func initDefaultConfig(confpath string) {
|
func initDefaultConfig(confpath string) {
|
||||||
config = Conf{
|
config = Conf{
|
||||||
"/dev/ttyS0",
|
SerialAddress: "/dev/ttyS0",
|
||||||
8888,
|
Port: 8888,
|
||||||
confpath + "/certificate.pem",
|
SslCertificate: confpath + "/certificate.pem",
|
||||||
confpath + "/privatekey.pem",
|
SslPrivatekey: confpath + "/privatekey.pem",
|
||||||
"device",
|
DisableAuth: false,
|
||||||
"enervent",
|
Username: "pingvin",
|
||||||
4,
|
Password: "enervent",
|
||||||
false,
|
Interval: 4,
|
||||||
"",
|
EnableMetrics: false,
|
||||||
false,
|
LogAccess: false,
|
||||||
false,
|
LogFile: "",
|
||||||
|
Debug: false,
|
||||||
|
ReadOnly: false,
|
||||||
}
|
}
|
||||||
conffile := confpath + "/configuration.yaml"
|
conffile := confpath + "/configuration.yaml"
|
||||||
confbytes, err := yaml.Marshal(&config)
|
confbytes, err := yaml.Marshal(&config)
|
||||||
|
@ -299,7 +157,7 @@ func initDefaultConfig(confpath string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read configuration. CLI flags take presedence over configuration file
|
// Read configuration. CLI flags take precedence over configuration file
|
||||||
func configure() {
|
func configure() {
|
||||||
log.Println("Reading configuration")
|
log.Println("Reading configuration")
|
||||||
parseConfigFile()
|
parseConfigFile()
|
||||||
|
@ -309,11 +167,13 @@ func configure() {
|
||||||
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.")
|
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")
|
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")
|
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")
|
usernflag := flag.String("username", config.Username, "Username for HTTP Basic Authentication")
|
||||||
passwflag := flag.String("password", config.Password, "Password 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")
|
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")
|
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")
|
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
|
// TODO: log file flag
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
config.Debug = *debugflag
|
config.Debug = *debugflag
|
||||||
|
@ -321,11 +181,13 @@ func configure() {
|
||||||
config.LogAccess = *logaccflag
|
config.LogAccess = *logaccflag
|
||||||
config.SslCertificate = *certflag
|
config.SslCertificate = *certflag
|
||||||
config.SslPrivatekey = *keyflag
|
config.SslPrivatekey = *keyflag
|
||||||
|
config.DisableAuth = *noauthflag
|
||||||
config.Username = *usernflag
|
config.Username = *usernflag
|
||||||
config.Password = *passwflag
|
config.Password = *passwflag
|
||||||
config.EnableMetrics = *promflag
|
config.EnableMetrics = *promflag
|
||||||
config.LogFile = *logflag
|
config.LogFile = *logflag
|
||||||
config.SerialAddress = *serialflag
|
config.SerialAddress = *serialflag
|
||||||
|
config.ReadOnly = *readOnly
|
||||||
usernamehash = sha256.Sum256([]byte(config.Username))
|
usernamehash = sha256.Sum256([]byte(config.Username))
|
||||||
passwordhash = sha256.Sum256([]byte(config.Password))
|
passwordhash = sha256.Sum256([]byte(config.Password))
|
||||||
if len(config.LogFile) != 0 {
|
if len(config.LogFile) != 0 {
|
||||||
|
|
|
@ -26,13 +26,14 @@ type pingvinCoil struct {
|
||||||
|
|
||||||
// unit modbus data
|
// unit modbus data
|
||||||
type Pingvin struct {
|
type Pingvin struct {
|
||||||
Coils []pingvinCoil
|
Coils []*pingvinCoil
|
||||||
Registers []pingvinRegister
|
Registers []*pingvinRegister
|
||||||
Status pingvinStatus
|
Status *pingvinStatus
|
||||||
buslock *sync.Mutex
|
buslock *sync.Mutex
|
||||||
handler *modbus.RTUClientHandler
|
handler *modbus.RTUClientHandler
|
||||||
modbusclient modbus.Client
|
modbusclient modbus.Client
|
||||||
Debug PingvinLogger
|
firstReadDone bool
|
||||||
|
Debug PingvinLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// single register data
|
// single register data
|
||||||
|
@ -67,13 +68,15 @@ type pingvinStatus struct {
|
||||||
HrcPct int `json:"hrc_pct"` // Heat recovery turn speed
|
HrcPct int `json:"hrc_pct"` // Heat recovery turn speed
|
||||||
TempSetting float32 `json:"temp_setting"` // Requested room temperature
|
TempSetting float32 `json:"temp_setting"` // Requested room temperature
|
||||||
FanPct int `json:"fan_pct"` // Circulation fan setting
|
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
|
Measurements pingvinMeasurements `json:"measurements"` // Measurements
|
||||||
HrcEffIn int `json:"hrc_efficiency_in"` // Calculated HRC efficiency, intake
|
HrcEffIn int `json:"hrc_efficiency_in"` // Calculated HRC efficiency, intake
|
||||||
HrcEffEx int `json:"hrc_efficiency_ex"` // Calculated HRC efficiency, extract
|
HrcEffEx int `json:"hrc_efficiency_ex"` // Calculated HRC efficiency, extract
|
||||||
OpMode string `json:"op_mode"` // Current operating mode, text representation
|
OpMode string `json:"op_mode"` // Current operating mode, text representation
|
||||||
Uptime string `json:"uptime"` // Unit uptime
|
Uptime string `json:"uptime"` // Unit uptime
|
||||||
SystemTime string `json:"system_time"` // Time and date in unit
|
SystemTime string `json:"system_time"` // Time and date in unit
|
||||||
Coils []pingvinCoil `json:"coils"`
|
Coils []*pingvinCoil `json:"coils"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PingvinLogger struct {
|
type PingvinLogger struct {
|
||||||
|
@ -101,7 +104,7 @@ 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)
|
addr, err := strconv.Atoi(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("newCoil: Atoi: ", err)
|
log.Fatal("newCoil: Atoi: ", err)
|
||||||
|
@ -111,7 +114,7 @@ func newCoil(address string, symbol string, description string) pingvinCoil {
|
||||||
promdesc := strings.ToLower(symbol)
|
promdesc := strings.ToLower(symbol)
|
||||||
zpadaddr := fmt.Sprintf("%02d", addr)
|
zpadaddr := fmt.Sprintf("%02d", addr)
|
||||||
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
|
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
|
||||||
return pingvinCoil{addr, symbol, false, description, reserved,
|
return &pingvinCoil{addr, symbol, false, description, reserved,
|
||||||
prometheus.NewDesc(
|
prometheus.NewDesc(
|
||||||
prometheus.BuildFQName("", "pingvin", promdesc),
|
prometheus.BuildFQName("", "pingvin", promdesc),
|
||||||
description,
|
description,
|
||||||
|
@ -120,10 +123,10 @@ func newCoil(address string, symbol string, description string) pingvinCoil {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pingvinCoil{addr, symbol, false, description, reserved, 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)
|
addr, err := strconv.Atoi(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("newRegister: Atoi(address): ", err)
|
log.Fatal("newRegister: Atoi(address): ", err)
|
||||||
|
@ -141,7 +144,7 @@ func newRegister(address, symbol, typ, multiplier, description string) pingvinRe
|
||||||
promdesc := strings.ToLower(symbol)
|
promdesc := strings.ToLower(symbol)
|
||||||
zpadaddr := fmt.Sprintf("%03d", addr)
|
zpadaddr := fmt.Sprintf("%03d", addr)
|
||||||
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
|
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
|
||||||
return pingvinRegister{
|
return &pingvinRegister{
|
||||||
addr,
|
addr,
|
||||||
symbol,
|
symbol,
|
||||||
0,
|
0,
|
||||||
|
@ -158,7 +161,7 @@ func newRegister(address, symbol, typ, multiplier, description string) pingvinRe
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl, nil}
|
return &pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read a CSV file containing data for coils or registers
|
// read a CSV file containing data for coils or registers
|
||||||
|
@ -210,11 +213,23 @@ func (p *Pingvin) Quit() {
|
||||||
|
|
||||||
// Update all coil values
|
// Update all coil values
|
||||||
func (p *Pingvin) updateCoils() {
|
func (p *Pingvin) updateCoils() {
|
||||||
p.buslock.Lock()
|
var results []byte
|
||||||
results, err := p.modbusclient.ReadCoils(0, uint16(len(p.Coils)))
|
var err error
|
||||||
p.buslock.Unlock()
|
for retries := 1; retries <= 5; retries++ {
|
||||||
if err != nil {
|
p.Debug.Println("Reading coils, attempt", retries)
|
||||||
log.Fatal("updateCoils: client.ReadCoils: ", err)
|
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,
|
// modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7,
|
||||||
// second byte coils 8-15 etc.
|
// second byte coils 8-15 etc.
|
||||||
|
@ -242,8 +257,8 @@ func (p *Pingvin) ReadRegister(addr uint16) (int, error) {
|
||||||
results, err := p.modbusclient.ReadHoldingRegisters(addr, 1)
|
results, err := p.modbusclient.ReadHoldingRegisters(addr, 1)
|
||||||
p.buslock.Unlock()
|
p.buslock.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("ERROR: ReadRegister:", err)
|
//log.Println("ERROR: ReadRegister:", err)
|
||||||
return 0, err
|
return p.Registers[addr].Value, err
|
||||||
}
|
}
|
||||||
if p.Registers[addr].Type == "uint16" {
|
if p.Registers[addr].Type == "uint16" {
|
||||||
p.Registers[addr].Value = int(uint16(results[0]) << 8)
|
p.Registers[addr].Value = int(uint16(results[0]) << 8)
|
||||||
|
@ -270,6 +285,7 @@ func (p *Pingvin) WriteRegister(addr uint16, value uint16) (uint16, error) {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if val == int(value) {
|
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 value, nil
|
||||||
}
|
}
|
||||||
return 0, fmt.Errorf("Failed to write register")
|
return 0, fmt.Errorf("Failed to write register")
|
||||||
|
@ -288,8 +304,8 @@ func (p *Pingvin) updateRegisters() {
|
||||||
if regs-k < 125 {
|
if regs-k < 125 {
|
||||||
r = regs - k
|
r = regs - k
|
||||||
}
|
}
|
||||||
results := []byte{}
|
var results []byte
|
||||||
for retries := 0; retries < 5; retries++ {
|
for retries := 1; retries <= 5; retries++ {
|
||||||
p.Debug.Println("Reading registers, attempt", retries, "k:", k)
|
p.Debug.Println("Reading registers, attempt", retries, "k:", k)
|
||||||
p.buslock.Lock()
|
p.buslock.Lock()
|
||||||
results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r))
|
results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r))
|
||||||
|
@ -297,11 +313,18 @@ func (p *Pingvin) updateRegisters() {
|
||||||
if len(results) > 0 {
|
if len(results) > 0 {
|
||||||
break
|
break
|
||||||
} else if retries == 4 {
|
} 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 {
|
} 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
|
// 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
|
// 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
|
// value, so for each even byte in the reponse slice we bitshift the byte
|
||||||
|
@ -350,22 +373,30 @@ func (p *Pingvin) Update() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read single coil
|
// Read single coil
|
||||||
func (p *Pingvin) ReadCoil(n uint16) ([]byte, error) {
|
func (p *Pingvin) ReadCoil(n uint16) (err error) {
|
||||||
p.buslock.Lock()
|
var results []byte
|
||||||
results, err := p.modbusclient.ReadCoils(n, 1)
|
for retries := 1; retries <= 5; retries++ {
|
||||||
p.buslock.Unlock()
|
p.buslock.Lock()
|
||||||
if err != nil {
|
results, err = p.modbusclient.ReadCoils(n, 1)
|
||||||
log.Fatal("ReadCoil: client.ReadCoils: ", err)
|
p.buslock.Unlock()
|
||||||
return nil, err
|
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
|
p.Coils[n].Value = results[0] == 1
|
||||||
return results, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force a single coil
|
// Force a single coil
|
||||||
func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
|
func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
|
||||||
if val {
|
if val {
|
||||||
p.checkMutexCoils(n, p.handler)
|
_ = p.checkMutexCoils(n) //, p.handler)
|
||||||
}
|
}
|
||||||
var value uint16 = 0
|
var value uint16 = 0
|
||||||
if val {
|
if val {
|
||||||
|
@ -377,14 +408,15 @@ func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("ERROR: WriteCoil: ", err)
|
log.Println("ERROR: WriteCoil: ", err)
|
||||||
}
|
}
|
||||||
if (val && results[0] == 255) || (!val && results[0] == 0) {
|
if (val && results[0] != 255) && (!val && results[0] != 0) {
|
||||||
log.Println("WriteCoil: wrote coil", n, "to value", val)
|
|
||||||
} else {
|
|
||||||
log.Println("ERROR: WriteCoil: failed to write coil")
|
log.Println("ERROR: WriteCoil: failed to write coil")
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,7 +472,7 @@ func (p *Pingvin) WriteCoils(startaddr uint16, quantity uint16, vals []bool) err
|
||||||
|
|
||||||
// Some of the coils are mutually exclusive, and can only be 1 one at a time.
|
// 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
|
// Check if coil is one of them and force all of them to 0 if so
|
||||||
func (p *Pingvin) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandler) error {
|
func (p *Pingvin) checkMutexCoils(addr uint16) error { //, handler *modbus.RTUClientHandler) error {
|
||||||
for _, mutexcoil := range mutexcoils {
|
for _, mutexcoil := range mutexcoils {
|
||||||
if mutexcoil == addr {
|
if mutexcoil == addr {
|
||||||
for _, n := range mutexcoils {
|
for _, n := range mutexcoils {
|
||||||
|
@ -462,6 +494,7 @@ func (p *Pingvin) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandler)
|
||||||
|
|
||||||
// populate p.Status struct for Home Assistant
|
// populate p.Status struct for Home Assistant
|
||||||
func (p *Pingvin) populateStatus() {
|
func (p *Pingvin) populateStatus() {
|
||||||
|
p.Status = &pingvinStatus{}
|
||||||
hpct := p.Registers[49].Value / p.Registers[49].Multiplier
|
hpct := p.Registers[49].Value / p.Registers[49].Multiplier
|
||||||
if hpct > 100 {
|
if hpct > 100 {
|
||||||
p.Status.HeaterPct = hpct - 100
|
p.Status.HeaterPct = hpct - 100
|
||||||
|
@ -472,6 +505,8 @@ func (p *Pingvin) populateStatus() {
|
||||||
}
|
}
|
||||||
p.Status.TempSetting = float32(p.Registers[135].Value) / float32(p.Registers[135].Multiplier)
|
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.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.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.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.SupplyHrc = float32(p.Registers[7].Value) / float32(p.Registers[7].Multiplier)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../index.html
|
|
|
@ -1 +0,0 @@
|
||||||
../index.html
|
|
Loading…
Reference in New Issue