Compare commits
49 Commits
Author | SHA1 | Date |
---|---|---|
Paul van Tilburg | 03c51e2a2c | |
Paul van Tilburg | cc1a0e4748 | |
Paul van Tilburg | d2b90c16f6 | |
Paul van Tilburg | d5eed08072 | |
Paul van Tilburg | 247e9d51f5 | |
Paul van Tilburg | 5241d90e79 | |
Paul van Tilburg | c61bbfef5b | |
Paul van Tilburg | f3ff02e4ff | |
Paul van Tilburg | f2be089fb9 | |
Paul van Tilburg | 45b3f52e71 | |
Paul van Tilburg | ff12875a08 | |
Paul van Tilburg | 1bf6a4e772 | |
Paul van Tilburg | c6f7511fc7 | |
Paul van Tilburg | 3fed86d36f | |
Paul van Tilburg | d3cc19524b | |
Paul van Tilburg | 92c75d09b9 | |
Paul van Tilburg | e1319dcfc2 | |
Paul van Tilburg | 0f1bc9d83d | |
Paul van Tilburg | 0068f6e9de | |
Paul van Tilburg | caad71389b | |
Paul van Tilburg | 0b76db96f0 | |
Paul van Tilburg | 42a43cc83d | |
Paul van Tilburg | 1aca61d3fd | |
Paul van Tilburg | 35dda781a3 | |
Paul van Tilburg | 14bda61a9e | |
Paul van Tilburg | bc22fd2d70 | |
Paul van Tilburg | fd00ef0b4f | |
Paul van Tilburg | c070877384 | |
Paul van Tilburg | 732d4b83f2 | |
Paul van Tilburg | 12a797baa9 | |
Paul van Tilburg | 5586ae4d70 | |
Paul van Tilburg | 59e3b53263 | |
Paul van Tilburg | f236499125 | |
Paul van Tilburg | 02a4d1ca9b | |
Paul van Tilburg | 3fff79a2cd | |
Paul van Tilburg | 9200a10cef | |
Paul van Tilburg | beb49373fb | |
Paul van Tilburg | 99e7e8a68c | |
Paul van Tilburg | bab9228b0f | |
Paul van Tilburg | 81e82e90da | |
Paul van Tilburg | b07bb73da4 | |
Paul van Tilburg | 58759d5309 | |
Paul van Tilburg | b1764b7fe3 | |
Paul van Tilburg | 7c704b69ed | |
Paul van Tilburg | 04e28a33c3 | |
Paul van Tilburg | de1ad37b95 | |
Paul van Tilburg | 5cbc3a04fc | |
Paul van Tilburg | ca116351db | |
Paul van Tilburg | 8d892d8619 |
|
@ -5,9 +5,6 @@ target
|
||||||
Dockerfile*
|
Dockerfile*
|
||||||
docker-compose*
|
docker-compose*
|
||||||
|
|
||||||
# Git folder
|
|
||||||
.git
|
|
||||||
|
|
||||||
# Dot files
|
# Dot files
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: "Check and lint using Cargo"
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request
|
||||||
|
- push
|
||||||
|
- workflow_dispatch
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_lint:
|
||||||
|
name: Check and lint
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Rust stable toolchain
|
||||||
|
uses: https://github.com/actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Run cargo check
|
||||||
|
uses: https://github.com/actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
|
||||||
|
- name: Run cargo clippy
|
||||||
|
uses: https://github.com/actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
args: -- -D warnings
|
||||||
|
|
||||||
|
- name: Run cargo fmt
|
||||||
|
uses: https://github.com/actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
|
# TODO: Add a test suite first!
|
||||||
|
# - name: Run cargo test
|
||||||
|
# uses: https://github.com/actions-rs/cargo@v1
|
||||||
|
# with:
|
||||||
|
# command: test
|
||||||
|
# args: --all-features
|
|
@ -0,0 +1,122 @@
|
||||||
|
name: "Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: "Release"
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine the version of the release
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
|
echo "Releasing version: $VERSION"
|
||||||
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Get the release notes from the changelog
|
||||||
|
run: |
|
||||||
|
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
|
||||||
|
RELEASE_NOTES=$(sed -n -e "/^## \[$VERSION\]/,/^## \[/{//"'!'"p;}" CHANGELOG.md | sed -e '1d;$d')
|
||||||
|
echo "Release notes:"
|
||||||
|
echo
|
||||||
|
echo "$RELEASE_NOTES"
|
||||||
|
echo "RELEASE_NOTES<<$EOF" >> "$GITHUB_ENV"
|
||||||
|
echo "$RELEASE_NOTES" >> "$GITHUB_ENV"
|
||||||
|
echo "$EOF" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '>=1.20.1'
|
||||||
|
|
||||||
|
- name: Release to Gitea
|
||||||
|
uses: actions/release-action@main
|
||||||
|
with:
|
||||||
|
# This is available by default.
|
||||||
|
api_key: '${{ secrets.RELEASE_TOKEN }}'
|
||||||
|
files: FIXME
|
||||||
|
title: 'Release ${{ env.VERSION }}'
|
||||||
|
body: '${{ env.RELEASE_NOTES }}'
|
||||||
|
|
||||||
|
release-crate:
|
||||||
|
name: "Release Rust crate"
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Rust stable toolchain
|
||||||
|
uses: https://github.com/actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Use sparse Cargo index for crates.io
|
||||||
|
run: echo -e '[registries.crates-io]\nprotocol = "sparse"' >> /root/.cargo/config.toml
|
||||||
|
|
||||||
|
- name: Register the Gitea crate registry with Cargo
|
||||||
|
run: echo -e '[registries.luon]\nindex = "https://git.luon.net/paul/_cargo-index.git"' >> /root/.cargo/config.toml
|
||||||
|
|
||||||
|
- name: Run cargo publish
|
||||||
|
uses: https://github.com/actions-rs/cargo@v1
|
||||||
|
env:
|
||||||
|
# This needs to be provided for the repository; no login necessary as a result.
|
||||||
|
CARGO_REGISTRIES_LUON_TOKEN: '${{ secrets.CARGO_TOKEN }}'
|
||||||
|
with:
|
||||||
|
command: publish
|
||||||
|
args: --registry luon
|
||||||
|
|
||||||
|
release-docker-image:
|
||||||
|
name: "Release Docker image"
|
||||||
|
runs-on: debian-latest
|
||||||
|
container:
|
||||||
|
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: https://github.com/docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
git.luon.net/paul/solar-grabber
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: https://github.com/docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to the Gitea Docker registry
|
||||||
|
uses: https://github.com/docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: git.luon.net
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
# This needs to be provided by the repository owner and have the packages scopes enabled.
|
||||||
|
# Note that the default `GITEA_TOKEN` secret does not have this scope enabled.
|
||||||
|
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Docker build and push
|
||||||
|
uses: https://github.com/docker/build-push-action@v4
|
||||||
|
env:
|
||||||
|
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
82
CHANGELOG.md
82
CHANGELOG.md
|
@ -7,6 +7,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.5] - 2024-02-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Fix clippy issue
|
||||||
|
|
||||||
|
## [0.3.4] - 2024-02-27
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
* Updated dependencies, fixes security advisories:
|
||||||
|
* [RUSTSEC-2024-0003](https://rustsec.org/advisories/RUSTSEC-2024-0003)
|
||||||
|
* [RUSTSEC-2023-0072](https://rustsec.org/advisories/RUSTSEC-2024-0072)
|
||||||
|
|
||||||
|
## [0.3.3] - 2023-11-03
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
* Update dependencies ([RUSTSEC-2020-0071](https://rustsec.org/advisories/RUSTSEC-2020-0071.html))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Switch to Rocket 0.5 RC4
|
||||||
|
|
||||||
|
## [0.3.2] - 2023-08-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Switch to Debian bookworm Docker image for runtime; fixes Docker image
|
||||||
|
|
||||||
|
## [0.3.1] - 2023-08-26
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Fix and improve Gitea Actions workflow
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
* Update dependencies ([RUSTSEC-2023-0044](https://rustsec.org/advisories/RUSTSEC-2023-0044))
|
||||||
|
|
||||||
|
## [0.3.0] - 2023-04-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Implement backoff for login/update API call failures (#8)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* Update dependencies
|
||||||
|
* Speed up Docker image builds by using sparse Cargo index for crates.io
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Fix login errors not being detected for My Autarco
|
||||||
|
* Fix missing build script/git repository during Docker image build
|
||||||
|
|
||||||
|
## [0.2.2] - 2023-03-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Implement error catchers for all endpoints (#5)
|
||||||
|
* Print the version on lift off (#6)
|
||||||
|
* Add `/version` endpoint to the API (#6)
|
||||||
|
* Add Gitea Actions workflow for cargo
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Fixed/tweaked documentation
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
* Update dependencies ([RUSTSEC-2023-0018](https://rustsec.org/advisories/RUSTSEC-2023-0018.html))
|
||||||
|
|
||||||
## [0.2.1] - 2023-01-16
|
## [0.2.1] - 2023-01-16
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -48,7 +121,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
Rename Autarco Scraper project to Solar Grabber.
|
Rename Autarco Scraper project to Solar Grabber.
|
||||||
|
|
||||||
[Unreleased]: https://git.luon.net/paul/solar-grabber/compare/v0.2.1...HEAD
|
[Unreleased]: https://git.luon.net/paul/solar-grabber/compare/v0.3.5...HEAD
|
||||||
|
[0.3.5]: https://git.luon.net/paul/solar-grabber/compare/v0.3.4...v0.3.5
|
||||||
|
[0.3.4]: https://git.luon.net/paul/solar-grabber/compare/v0.3.3...v0.3.4
|
||||||
|
[0.3.3]: https://git.luon.net/paul/solar-grabber/compare/v0.3.2...v0.3.3
|
||||||
|
[0.3.2]: https://git.luon.net/paul/solar-grabber/compare/v0.3.1...v0.3.2
|
||||||
|
[0.3.1]: https://git.luon.net/paul/solar-grabber/compare/v0.3.0...v0.3.1
|
||||||
|
[0.3.0]: https://git.luon.net/paul/solar-grabber/compare/v0.2.2...v0.3.0
|
||||||
|
[0.2.2]: https://git.luon.net/paul/solar-grabber/compare/v0.2.1...v0.2.2
|
||||||
[0.2.1]: https://git.luon.net/paul/solar-grabber/compare/v0.2.0...v0.2.1
|
[0.2.1]: https://git.luon.net/paul/solar-grabber/compare/v0.2.0...v0.2.1
|
||||||
[0.2.0]: https://git.luon.net/paul/solar-grabber/compare/v0.1.1...v0.2.0
|
[0.2.0]: https://git.luon.net/paul/solar-grabber/compare/v0.1.1...v0.2.0
|
||||||
[0.1.1]: https://git.luon.net/paul/solar-grabber/src/tag/v0.1.1
|
[0.1.1]: https://git.luon.net/paul/solar-grabber/src/tag/v0.1.1
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "solar-grabber"
|
name = "solar-grabber"
|
||||||
version = "0.2.1"
|
version = "0.3.5"
|
||||||
authors = ["Paul van Tilburg <paul@luon.net>"]
|
authors = ["Paul van Tilburg <paul@luon.net>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = """"
|
description = """"
|
||||||
|
@ -10,6 +10,7 @@ get statistical data of your solar panels.
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://git.luon.net/paul/solar-grabber"
|
repository = "https://git.luon.net/paul/solar-grabber"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.23", features = ["serde"] }
|
chrono = { version = "0.4.23", features = ["serde"] }
|
||||||
|
@ -18,11 +19,13 @@ enum_dispatch = "0.3.9"
|
||||||
md-5 = "0.10.5"
|
md-5 = "0.10.5"
|
||||||
once_cell = "1.9.0"
|
once_cell = "1.9.0"
|
||||||
reqwest = { version = "0.11.6", features = ["cookies", "json"] }
|
reqwest = { version = "0.11.6", features = ["cookies", "json"] }
|
||||||
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
rocket = { version = "0.5.0-rc.3", features = ["json"] }
|
||||||
thiserror = "1.0.38"
|
thiserror = "1.0.38"
|
||||||
toml = "0.5.6"
|
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
vergen = { version = "8.1.1", features = ["build", "git", "gitcl"] }
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
maintainer = "Paul van Tilburg <paul@luon.net>"
|
maintainer = "Paul van Tilburg <paul@luon.net>"
|
||||||
copyright = "2022, Paul van Tilburg"
|
copyright = "2022, Paul van Tilburg"
|
||||||
|
|
|
@ -10,10 +10,12 @@ FROM docker.io/rust:1 as builder
|
||||||
RUN USER=root cargo new --bin /usr/src/solar-grabber
|
RUN USER=root cargo new --bin /usr/src/solar-grabber
|
||||||
WORKDIR /usr/src/solar-grabber
|
WORKDIR /usr/src/solar-grabber
|
||||||
COPY ./Cargo.* ./
|
COPY ./Cargo.* ./
|
||||||
|
RUN sed -i -e 's/^build =/#build =/' Cargo.toml
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
RUN rm src/*.rs
|
RUN rm src/*.rs
|
||||||
|
|
||||||
# Add the real project files from current folder
|
# Add the real project files from current folder
|
||||||
|
COPY ./Cargo.toml ./
|
||||||
ADD . ./
|
ADD . ./
|
||||||
|
|
||||||
# Build the actual binary from the copied local files
|
# Build the actual binary from the copied local files
|
||||||
|
@ -22,7 +24,7 @@ RUN cargo build --release
|
||||||
|
|
||||||
########################## RUNTIME IMAGE ##########################
|
########################## RUNTIME IMAGE ##########################
|
||||||
# Create new stage with a minimal image for the actual runtime image/container
|
# Create new stage with a minimal image for the actual runtime image/container
|
||||||
FROM docker.io/debian:bullseye-slim
|
FROM docker.io/debian:bookworm-slim
|
||||||
|
|
||||||
# Install CA certificates
|
# Install CA certificates
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
|
|
36
README.md
36
README.md
|
@ -82,7 +82,7 @@ This also uses `Rocket.toml` from the current working directory as configuration
|
||||||
You can alternatively pass a set of environment variables instead. See
|
You can alternatively pass a set of environment variables instead. See
|
||||||
`docker-compose.yml` for a list.
|
`docker-compose.yml` for a list.
|
||||||
|
|
||||||
## API endpoint
|
## Status API Endpoint
|
||||||
|
|
||||||
The `/` API endpoint provides the current statistical data of your solar panels
|
The `/` API endpoint provides the current statistical data of your solar panels
|
||||||
once it has successfully logged into the cloud service using your credentials.
|
once it has successfully logged into the cloud service using your credentials.
|
||||||
|
@ -92,9 +92,9 @@ There is no path and no query parameters, just:
|
||||||
GET /
|
GET /
|
||||||
```
|
```
|
||||||
|
|
||||||
### Response
|
### Status API Response
|
||||||
|
|
||||||
A response uses the JSON format and typically looks like this:
|
The response uses the JSON format and typically looks like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"current_w":23.0,"total_kwh":6159.0,"last_updated":1661194620}
|
{"current_w":23.0,"total_kwh":6159.0,"last_updated":1661194620}
|
||||||
|
@ -104,6 +104,36 @@ This contains the current production power (`current_w`) in Watt,
|
||||||
the total of produced energy since installation (`total_kwh`) in kilowatt-hour
|
the total of produced energy since installation (`total_kwh`) in kilowatt-hour
|
||||||
and the (UNIX) timestamp that indicates when the information was last updated.
|
and the (UNIX) timestamp that indicates when the information was last updated.
|
||||||
|
|
||||||
|
### (Status) API Error Response
|
||||||
|
|
||||||
|
If the API endpoint is accessed before any statistical data has been retrieved,
|
||||||
|
or if any other request than `GET /` is made, an error response is returned
|
||||||
|
that looks like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"error":"No status found (yet)"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version API Endpoint
|
||||||
|
|
||||||
|
The `/version` endpoint provides information of the current version and build
|
||||||
|
of the service. This can be used to check if it needs to be updated.
|
||||||
|
Again, there is no path and no query parameters, just:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version API Response
|
||||||
|
|
||||||
|
The response uses the JSON format and typically looks like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"version":"0.2.1","timestamp":"2023-01-29T14:10:24.971748027Z","git_sha":"5cbc3a04","git_timestamp":"2023-01-16T20:18:20Z"}
|
||||||
|
```
|
||||||
|
|
||||||
|
(Build and git information may be out of date.)
|
||||||
|
|
||||||
## Integration in Home Assistant
|
## Integration in Home Assistant
|
||||||
|
|
||||||
To integrate the Solar Grabber service in your [Home Assistant](https://www.home-assistant.io/)
|
To integrate the Solar Grabber service in your [Home Assistant](https://www.home-assistant.io/)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use vergen::EmitBuilder;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
// Generate the `cargo:` instructions to fill the appropriate environment variables.
|
||||||
|
EmitBuilder::builder().all_build().all_git().emit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
95
src/lib.rs
95
src/lib.rs
|
@ -20,12 +20,12 @@ mod update;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rocket::fairing::AdHoc;
|
|
||||||
use rocket::serde::json::Json;
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
|
catch, catchers,
|
||||||
|
fairing::AdHoc,
|
||||||
get, routes,
|
get, routes,
|
||||||
serde::{Deserialize, Serialize},
|
serde::{json::Json, Deserialize, Serialize},
|
||||||
Build, Rocket,
|
Build, Request, Rocket,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::update::update_loop;
|
use self::update::update_loop;
|
||||||
|
@ -45,25 +45,87 @@ struct Config {
|
||||||
#[derive(Clone, Copy, Debug, Serialize)]
|
#[derive(Clone, Copy, Debug, Serialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
struct Status {
|
struct Status {
|
||||||
/// Current power production (W)
|
/// The current power production (W).
|
||||||
current_w: f32,
|
current_w: f32,
|
||||||
/// Total energy produced since installation (kWh)
|
/// The total energy produced since installation (kWh).
|
||||||
total_kwh: f32,
|
total_kwh: f32,
|
||||||
/// Timestamp of last update
|
/// The (UNIX) timestamp of when the status was last updated.
|
||||||
last_updated: u64,
|
last_updated: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An error used as JSON response.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
struct Error {
|
||||||
|
/// The error message.
|
||||||
|
error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
/// Creates a new error result from a message.
|
||||||
|
fn from(message: impl AsRef<str>) -> Self {
|
||||||
|
let error = String::from(message.as_ref());
|
||||||
|
|
||||||
|
Self { error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the current (last known) status.
|
/// Returns the current (last known) status.
|
||||||
#[get("/", format = "application/json")]
|
#[get("/", format = "application/json")]
|
||||||
async fn status() -> Option<Json<Status>> {
|
async fn status() -> Result<Json<Status>, Json<Error>> {
|
||||||
let status_guard = STATUS.lock().expect("Status mutex was poisoined");
|
let status_guard = STATUS.lock().expect("Status mutex was poisoined");
|
||||||
status_guard.map(Json)
|
status_guard
|
||||||
|
.map(Json)
|
||||||
|
.ok_or_else(|| Json(Error::from("No status found (yet)")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The version information as JSON response.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
struct VersionInfo {
|
||||||
|
/// The version of the build.
|
||||||
|
version: String,
|
||||||
|
/// The timestamp of the build.
|
||||||
|
timestamp: String,
|
||||||
|
/// The (most recent) git SHA used for the build.
|
||||||
|
git_sha: String,
|
||||||
|
/// The timestamp of the last git commit used for the build.
|
||||||
|
git_timestamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VersionInfo {
|
||||||
|
/// Retrieves the version information from the environment variables.
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
version: String::from(env!("CARGO_PKG_VERSION")),
|
||||||
|
timestamp: String::from(env!("VERGEN_BUILD_TIMESTAMP")),
|
||||||
|
git_sha: String::from(&env!("VERGEN_GIT_SHA")[0..7]),
|
||||||
|
git_timestamp: String::from(env!("VERGEN_GIT_COMMIT_TIMESTAMP")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the version information.
|
||||||
|
#[get("/version", format = "application/json")]
|
||||||
|
async fn version() -> Result<Json<VersionInfo>, Json<Error>> {
|
||||||
|
Ok(Json(VersionInfo::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default catcher for any unsuppored request
|
||||||
|
#[catch(default)]
|
||||||
|
fn unsupported(status: rocket::http::Status, _request: &Request<'_>) -> Json<Error> {
|
||||||
|
let code = status.code;
|
||||||
|
|
||||||
|
Json(Error::from(format!(
|
||||||
|
"Unhandled/unsupported API call or path (HTTP {code})"
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Rocket and attaches the config parsing and update loop as fairings.
|
/// Creates a Rocket and attaches the config parsing and update loop as fairings.
|
||||||
pub fn setup() -> Rocket<Build> {
|
pub fn setup() -> Rocket<Build> {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", routes![status])
|
.mount("/", routes![status, version])
|
||||||
|
.register("/", catchers![unsupported])
|
||||||
.attach(AdHoc::config::<Config>())
|
.attach(AdHoc::config::<Config>())
|
||||||
.attach(AdHoc::on_liftoff("Updater", |rocket| {
|
.attach(AdHoc::on_liftoff("Updater", |rocket| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
@ -73,8 +135,17 @@ pub fn setup() -> Rocket<Build> {
|
||||||
.expect("Invalid configuration");
|
.expect("Invalid configuration");
|
||||||
let service = services::get(config.service).expect("Invalid service");
|
let service = services::get(config.service).expect("Invalid service");
|
||||||
|
|
||||||
// We don't care about the join handle nor error results?t
|
// We don't care about the join handle nor error results?
|
||||||
let _ = rocket::tokio::spawn(update_loop(service));
|
let _service = rocket::tokio::spawn(update_loop(service));
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.attach(AdHoc::on_liftoff("Version", |_| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let name = env!("CARGO_PKG_NAME");
|
||||||
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
let git_sha = &env!("VERGEN_GIT_SHA")[0..7];
|
||||||
|
|
||||||
|
println!("☀️ Started {name} v{version} (git @{git_sha})");
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use chrono::{DateTime, Local, TimeZone};
|
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
||||||
use md5::{Digest, Md5};
|
use md5::{Digest, Md5};
|
||||||
use reqwest::{cookie::Jar as CookieJar, Client, ClientBuilder, Url};
|
use reqwest::{cookie::Jar as CookieJar, Client, ClientBuilder, Url};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
|
@ -112,7 +112,7 @@ where
|
||||||
use rocket::serde::de::Error;
|
use rocket::serde::de::Error;
|
||||||
|
|
||||||
match <StringOrObject<'_, T>>::deserialize(deserializer) {
|
match <StringOrObject<'_, T>>::deserialize(deserializer) {
|
||||||
Ok(StringOrObject::String(s)) if s.is_empty() => Ok(None),
|
Ok(StringOrObject::String("")) => Ok(None),
|
||||||
Ok(StringOrObject::String(_)) => Err(Error::custom("Non-empty string not allowed here")),
|
Ok(StringOrObject::String(_)) => Err(Error::custom("Non-empty string not allowed here")),
|
||||||
Ok(StringOrObject::Object(t)) => Ok(Some(t)),
|
Ok(StringOrObject::Object(t)) => Ok(Some(t)),
|
||||||
Ok(StringOrObject::Value(j)) => Err(Error::custom(&format!(
|
Ok(StringOrObject::Value(j)) => Err(Error::custom(&format!(
|
||||||
|
@ -133,9 +133,12 @@ where
|
||||||
use rocket::serde::de::Error;
|
use rocket::serde::de::Error;
|
||||||
|
|
||||||
let s = <&str>::deserialize(deserializer)?;
|
let s = <&str>::deserialize(deserializer)?;
|
||||||
|
let dt = NaiveDateTime::parse_from_str(s, DATE_TIME_FORMAT).map_err(D::Error::custom)?;
|
||||||
|
|
||||||
Local
|
Local
|
||||||
.datetime_from_str(s, DATE_TIME_FORMAT)
|
.from_local_datetime(&dt)
|
||||||
.map_err(D::Error::custom)
|
.latest()
|
||||||
|
.ok_or_else(|| D::Error::custom("time representation is invalid for server time zone"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserializes a string ([`&str`]) into a float ([`f32`]).
|
/// Deserializes a string ([`&str`]) into a float ([`f32`]).
|
||||||
|
|
|
@ -92,14 +92,17 @@ impl super::Service for Service {
|
||||||
/// It mainly stores the acquired cookie in the client's cookie jar. The login credentials come
|
/// It mainly stores the acquired cookie in the client's cookie jar. The login credentials come
|
||||||
/// from the loaded configuration (see [`Config`]).
|
/// from the loaded configuration (see [`Config`]).
|
||||||
async fn login(&mut self) -> Result<()> {
|
async fn login(&mut self) -> Result<()> {
|
||||||
|
let login_url = login_url().expect("valid login URL");
|
||||||
let params = [
|
let params = [
|
||||||
("username", &self.config.username),
|
("username", &self.config.username),
|
||||||
("password", &self.config.password),
|
("password", &self.config.password),
|
||||||
];
|
];
|
||||||
let login_url = login_url().expect("valid login URL");
|
let response = self.client.post(login_url).form(¶ms).send().await?;
|
||||||
self.client.post(login_url).form(¶ms).send().await?;
|
match response.error_for_status() {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
Ok(())
|
Err(e) if e.status() == Some(StatusCode::UNAUTHORIZED) => Err(Error::NotAuthorized),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a status update from the API of the My Autarco site.
|
/// Retrieves a status update from the API of the My Autarco site.
|
||||||
|
|
|
@ -9,6 +9,22 @@ use crate::{
|
||||||
STATUS,
|
STATUS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The default sleep interval to use between checks.
|
||||||
|
const DEFAULT_SLEEP_INTERVAL: u64 = 10;
|
||||||
|
|
||||||
|
/// The sleep interval upper limit when applying exponential backoff.
|
||||||
|
const MAX_SLEEP_INTERVAL: u64 = 320;
|
||||||
|
|
||||||
|
/// The backoff factor.
|
||||||
|
const BACKOFF_FACTOR: f64 = 2.0;
|
||||||
|
|
||||||
|
/// Calculates the new interval by applying the backoff factor and taking the maximum into account.
|
||||||
|
fn back_off(interval: u64) -> u64 {
|
||||||
|
let new_interval = (interval as f64 * BACKOFF_FACTOR) as u64;
|
||||||
|
|
||||||
|
new_interval.min(MAX_SLEEP_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
/// Main update loop that logs in and periodically acquires updates from the API.
|
/// Main update loop that logs in and periodically acquires updates from the API.
|
||||||
///
|
///
|
||||||
/// It updates the mutex-guarded current update [`Status`](crate::Status) struct which can be
|
/// It updates the mutex-guarded current update [`Status`](crate::Status) struct which can be
|
||||||
|
@ -23,9 +39,10 @@ pub(super) async fn update_loop(service: Services) -> color_eyre::Result<()> {
|
||||||
|
|
||||||
let mut last_updated = 0;
|
let mut last_updated = 0;
|
||||||
let poll_interval = service.poll_interval();
|
let poll_interval = service.poll_interval();
|
||||||
|
let mut sleep_interval = DEFAULT_SLEEP_INTERVAL;
|
||||||
loop {
|
loop {
|
||||||
// Wake up every 10 seconds and check if an update is due.
|
// Wake up every 10 seconds and check if an update is due.
|
||||||
sleep(Duration::from_secs(10)).await;
|
sleep(Duration::from_secs(sleep_interval)).await;
|
||||||
|
|
||||||
let timestamp = SystemTime::now()
|
let timestamp = SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
@ -39,15 +56,24 @@ pub(super) async fn update_loop(service: Services) -> color_eyre::Result<()> {
|
||||||
Ok(status) => status,
|
Ok(status) => status,
|
||||||
Err(Error::NotAuthorized) => {
|
Err(Error::NotAuthorized) => {
|
||||||
eprintln!("💥 Update unauthorized, trying to log in again...");
|
eprintln!("💥 Update unauthorized, trying to log in again...");
|
||||||
service.login().await?;
|
if let Err(e) = service.login().await {
|
||||||
|
eprintln!("💥 Login failed: {e}; will retry in {sleep_interval} seconds...");
|
||||||
|
sleep_interval = back_off(sleep_interval);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
println!("⚡ Logged in successfully!");
|
println!("⚡ Logged in successfully!");
|
||||||
|
sleep_interval = DEFAULT_SLEEP_INTERVAL;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("💥 Failed to update status: {}", e);
|
eprintln!(
|
||||||
|
"💥 Failed to update status: {e}; will retry in {sleep_interval} seconds..."
|
||||||
|
);
|
||||||
|
sleep_interval = back_off(sleep_interval);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
sleep_interval = DEFAULT_SLEEP_INTERVAL;
|
||||||
last_updated = timestamp;
|
last_updated = timestamp;
|
||||||
|
|
||||||
println!("⚡ Updated status to: {:#?}", status);
|
println!("⚡ Updated status to: {:#?}", status);
|
||||||
|
|
Loading…
Reference in New Issue