Split project (keep common)
This commit is contained in:
parent
9bc75b9627
commit
96a64cf90d
68 changed files with 32 additions and 3502 deletions
|
@ -1,3 +0,0 @@
|
|||
[alias]
|
||||
format = "+nightly fmt"
|
||||
lint = "clippy --all-targets --all-features -- -D warnings"
|
|
@ -1,3 +0,0 @@
|
|||
#EMGAUWA_CONTROLLER__LOGGING__LEVEL=DEBUG
|
||||
|
||||
#EMGAUWA_CORE__LOGGING__LEVEL=DEBUG
|
39
Cargo.toml
39
Cargo.toml
|
@ -1,7 +1,32 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"emgauwa-core",
|
||||
"emgauwa-controller",
|
||||
"emgauwa-common",
|
||||
]
|
||||
[package]
|
||||
name = "emgauwa-common"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["Tobias Reisinger <tobias@msrg.cc>"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
actix = "0.13"
|
||||
actix-web = "4.4"
|
||||
actix-web-actors = "4.2"
|
||||
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
simple_logger = "4.2"
|
||||
log = "0.4"
|
||||
|
||||
config = "0.13"
|
||||
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros", "chrono"] }
|
||||
libsqlite3-sys = { version = "*", features = ["bundled"] }
|
||||
uuid = "1.6"
|
||||
futures = "0.3"
|
||||
libc = "0.2"
|
||||
|
||||
rppal = "0.17"
|
||||
rppal-pfd = "0.0.5"
|
||||
rppal-mcp23s17 = "0.0.3"
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
[build]
|
||||
pre-build = [
|
||||
"curl -Lo /usr/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64",
|
||||
"chmod +x /usr/bin/yq"
|
||||
]
|
37
Makefile
37
Makefile
|
@ -3,40 +3,3 @@ sqlx:
|
|||
cargo sqlx database create
|
||||
cargo sqlx migrate run
|
||||
cargo sqlx prepare --workspace
|
||||
|
||||
build-rpi:
|
||||
cross build --target arm-unknown-linux-gnueabihf
|
||||
|
||||
emgauwa-%.json: config/%.pkl
|
||||
pkl eval -f json -o $@ $<
|
||||
|
||||
configs:
|
||||
$(MAKE) emgauwa-core.json
|
||||
$(MAKE) emgauwa-controller.json
|
||||
|
||||
clean:
|
||||
rm -f emgauwa-controller.json
|
||||
rm -f emgauwa-controller.sqlite
|
||||
rm -f emgauwa-core.json
|
||||
rm -f emgauwa-core.sqlite
|
||||
rm -f emgauwa-dev.sqlite
|
||||
|
||||
emgauwa-controller_%:
|
||||
$(TOOL) build --target $* --release --bin emgauwa-controller
|
||||
mkdir -p out/releases
|
||||
cp target/$*/release/emgauwa-controller out/releases/emgauwa-controller_$*
|
||||
|
||||
emgauwa-core_%:
|
||||
$(TOOL) build --target $* --release --bin emgauwa-core
|
||||
mkdir -p out/releases
|
||||
cp target/$*/release/emgauwa-core out/releases/emgauwa-core_$*
|
||||
|
||||
emgauwa_%:
|
||||
$(MAKE) emgauwa-controller_$*
|
||||
$(MAKE) emgauwa-core_$*
|
||||
|
||||
releases:
|
||||
$(MAKE) TOOL=cross emgauwa_arm-unknown-linux-gnueabihf
|
||||
$(MAKE) TOOL=cargo emgauwa_x86_64-unknown-linux-gnu
|
||||
$(MAKE) TOOL=cross emgauwa_x86_64-unknown-linux-musl
|
||||
|
||||
|
|
875
api.v1.yaml
875
api.v1.yaml
|
@ -1,875 +0,0 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
contact:
|
||||
name: Tobias Reisinger
|
||||
url: 'https://git.serguzim.me/emgauwa/'
|
||||
title: Emgauwa API v1
|
||||
version: 0.5.0
|
||||
description: Server API to manage an Emgauwa system.
|
||||
servers:
|
||||
- url: 'http://localhost:4419'
|
||||
tags:
|
||||
- name: schedules
|
||||
- name: relays
|
||||
- name: controllers
|
||||
- name: tags
|
||||
- name: macros
|
||||
- name: websocket
|
||||
paths:
|
||||
/api/v1/schedules:
|
||||
get:
|
||||
summary: get all schedules
|
||||
description: Receive a list with all available schedules.
|
||||
tags:
|
||||
- schedules
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
headers: { }
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
operationId: get-api-v1-schedules
|
||||
post:
|
||||
summary: add new schedule
|
||||
tags:
|
||||
- schedules
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
operationId: post-api-v1-schedules
|
||||
description: Create a new schedule. A new unique id will be returned
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
description: The "id" field will be set by the server.
|
||||
parameters: [ ]
|
||||
'/api/v1/schedules/{schedule_id}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
format: uuid
|
||||
name: schedule_id
|
||||
in: path
|
||||
required: true
|
||||
description: ''
|
||||
get:
|
||||
summary: get single schedule
|
||||
tags:
|
||||
- schedules
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
operationId: get-schedules-schedule_id
|
||||
description: Return a single schedule by id.
|
||||
put:
|
||||
summary: overwrite single schedule
|
||||
tags:
|
||||
- schedules
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
'400':
|
||||
description: Bad Request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
operationId: put-schedules-schedule_id
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
periods:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/period'
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/tag'
|
||||
description: ''
|
||||
parameters: [ ]
|
||||
description: Overwrite the properties for a single schedule. Overwriting periods on "on" or "off" will fail.
|
||||
delete:
|
||||
summary: delete single schedule
|
||||
tags:
|
||||
- schedules
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'403':
|
||||
description: Forbidden
|
||||
'404':
|
||||
description: Not Found
|
||||
operationId: delete-schedules-schedule_id
|
||||
description: Deletes a single schedule. Deleting "on" or "off" is forbidden (403).
|
||||
'/api/v1/schedules/tag/{tag}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: tag
|
||||
in: path
|
||||
required: true
|
||||
description: ''
|
||||
get:
|
||||
summary: get schedules by tag
|
||||
tags:
|
||||
- schedules
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
operationId: get-schedules-tag-schedule_id
|
||||
description: Receive a list of schedules which include the given tag.
|
||||
/api/v1/relays:
|
||||
get:
|
||||
summary: get all relays
|
||||
tags:
|
||||
- relays
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
headers: { }
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/relay'
|
||||
operationId: get-relays
|
||||
description: Return a list with all relays.
|
||||
parameters: [ ]
|
||||
'/api/v1/relays/tag/{tag}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: tag
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: get relays by tag
|
||||
tags:
|
||||
- relays
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/relay'
|
||||
operationId: get-relays-tag-tag
|
||||
description: Return all relays with the given tag.
|
||||
/api/v1/controllers:
|
||||
get:
|
||||
summary: get all controllers
|
||||
tags:
|
||||
- controllers
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/controller'
|
||||
operationId: get-controllers
|
||||
description: Return all controllers.
|
||||
parameters: [ ]
|
||||
'/api/v1/controllers/{controller_id}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: controller_id
|
||||
in: path
|
||||
description: ''
|
||||
required: true
|
||||
get:
|
||||
summary: get single controller
|
||||
tags:
|
||||
- controllers
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/controller'
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
operationId: get-controllers-controller_id
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
description: ''
|
||||
description: Return a single controller by id. When no controller with the id is found 404 will be returned.
|
||||
put:
|
||||
summary: overwrite single controller
|
||||
tags:
|
||||
- controllers
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/controller'
|
||||
'400':
|
||||
description: Bad Request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
operationId: put-controllers-controller_id
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
ip:
|
||||
type: string
|
||||
format: ipv4
|
||||
description: Overwrite properties of a single controller.
|
||||
delete:
|
||||
summary: delete single controller
|
||||
tags:
|
||||
- controllers
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'404':
|
||||
description: Not Found
|
||||
operationId: delete-controllers-controller_id
|
||||
description: Delete a single controller. To recover the controller you need to use the controllers/discover feature.
|
||||
'/api/v1/controllers/{controller_id}/relays':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: controller_id
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: get all relays for single controller
|
||||
tags:
|
||||
- controllers
|
||||
- relays
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
headers: { }
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/relay'
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { }
|
||||
operationId: get-controllers-controller_id-relays
|
||||
description: Returns all relays for a single controller.
|
||||
'/api/v1/controllers/{controller_id}/relays/{relay_num}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: controller_id
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: integer
|
||||
name: relay_num
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: get single relay for single controller
|
||||
tags:
|
||||
- controllers
|
||||
- relays
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/relay'
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
operationId: get-controllers-controller_id-relays-relay_num
|
||||
description: 'Return a single relay by number for a controller by id. When the relay or controller is not found, 404 will be returned.'
|
||||
put:
|
||||
summary: overwrite single relay for single controller
|
||||
tags:
|
||||
- controllers
|
||||
- relays
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/relay'
|
||||
'400':
|
||||
description: Bad Request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
'404':
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: { }
|
||||
operationId: put-controllers-controller_id-relays-relay_num
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
active_schedule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/schedule_id'
|
||||
schedules:
|
||||
type: array
|
||||
maxItems: 7
|
||||
minItems: 7
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/schedule_id'
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/tag'
|
||||
description: 'active schedule will overwrite schedules[weekday]'
|
||||
/api/v1/tags:
|
||||
get:
|
||||
summary: get all tags
|
||||
tags:
|
||||
- tags
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/tag'
|
||||
operationId: get-tags
|
||||
description: Returns a list of tags.
|
||||
parameters: [ ]
|
||||
post:
|
||||
summary: add new tag
|
||||
operationId: post-api-v1-tags
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/tag_full'
|
||||
'400':
|
||||
description: Bad Request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
tag:
|
||||
$ref: '#/components/schemas/tag'
|
||||
description: ''
|
||||
tags:
|
||||
- tags
|
||||
description: Add a new tag. Will return 400 when the tag already exits.
|
||||
'/api/v1/tags/{tag}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: tag
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: get relays and schedules for tag
|
||||
tags:
|
||||
- tags
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/tag_full'
|
||||
'404':
|
||||
description: Not Found
|
||||
operationId: get-tags-tag
|
||||
description: Return all models with the given tag (relays and schedules)
|
||||
delete:
|
||||
summary: delete tag
|
||||
operationId: delete-tags-tag
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'404':
|
||||
description: Not Found
|
||||
description: delete tag from database and from affected relays and schedules
|
||||
tags:
|
||||
- tags
|
||||
'/api/v1/controllers/{controller_id}/relays/{relay_num}/pulse':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: controller_id
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: relay_num
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: pulse relay on
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'404':
|
||||
description: Not Found
|
||||
operationId: post-controllers-controller_id-relays-relay_num-pulse
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
duration:
|
||||
type: integer
|
||||
description: ''
|
||||
description: Turn a relay on for a short amount of time. The duration can be set in the body in seconds. When no duration is supplied the default for the relay will be used. The default is read from the controller's config.
|
||||
tags:
|
||||
- controllers
|
||||
- relays
|
||||
/api/v1/ws/relays:
|
||||
get:
|
||||
summary: get relay status updates
|
||||
tags:
|
||||
- websocket
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/relay'
|
||||
operationId: get-ws-relays
|
||||
description: |-
|
||||
WEBSOCKET
|
||||
This websocket will send all relays with the most recent status every 10 seconds.
|
||||
parameters: [ ]
|
||||
/api/v1/macros:
|
||||
get:
|
||||
summary: get all macros
|
||||
tags:
|
||||
- macros
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/macro'
|
||||
operationId: get-api-v1-macros
|
||||
description: Receive a list with all available macros.
|
||||
post:
|
||||
summary: add new macro
|
||||
tags:
|
||||
- macros
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
operationId: post-api-v1-macros
|
||||
description: Create a new macro. A new unique id will be returned
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
weekday:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 6
|
||||
relay:
|
||||
type: object
|
||||
properties:
|
||||
number:
|
||||
type: integer
|
||||
controller_id:
|
||||
$ref: '#/components/schemas/controller_id'
|
||||
schedule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/schedule_id'
|
||||
'/api/v1/macros/{macro_id}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: macro_id
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: get a single macro
|
||||
tags:
|
||||
- macros
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/macro'
|
||||
operationId: get-api-v1-macros-macro_id
|
||||
description: Return a single macro by id. When no macro with the id is found 404 will be returned.
|
||||
put:
|
||||
summary: overwrite a macro
|
||||
tags:
|
||||
- macros
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/macro'
|
||||
operationId: put-api-v1-macros-macro_id
|
||||
description: Overwrite properties of a single macro.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
weekday:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 6
|
||||
schedule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/schedule_id'
|
||||
relay:
|
||||
type: object
|
||||
properties:
|
||||
number:
|
||||
type: integer
|
||||
controller_id:
|
||||
$ref: '#/components/schemas/controller_id'
|
||||
delete:
|
||||
summary: delete a macro
|
||||
tags:
|
||||
- macros
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
operationId: delete-api-v1-macros-macro_id
|
||||
description: Delete a single macro.
|
||||
'/api/v1/macros/{macro_id}/execute':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: macro_id
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: integer
|
||||
name: weekday
|
||||
in: query
|
||||
put:
|
||||
summary: execute a macro
|
||||
tags:
|
||||
- macros
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'404':
|
||||
description: Not Found
|
||||
operationId: put-api-v1-macros-macro_id-execute
|
||||
description: Execute a macro
|
||||
/api/v1/schedules/list:
|
||||
post:
|
||||
summary: add new schedule list
|
||||
tags:
|
||||
- schedules
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
operationId: post-schedules-list
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
description: Create a list of schedules
|
||||
parameters: [ ]
|
||||
components:
|
||||
schemas:
|
||||
controller:
|
||||
title: controller
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/controller_id'
|
||||
name:
|
||||
type: string
|
||||
example: Garden Controller
|
||||
ip:
|
||||
type: string
|
||||
format: ipv4
|
||||
example: 224.73.153.12
|
||||
active:
|
||||
type: boolean
|
||||
port:
|
||||
type: integer
|
||||
example: 27480
|
||||
relay_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
example: 10
|
||||
relays:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/relay'
|
||||
relay:
|
||||
title: relay
|
||||
type: object
|
||||
properties:
|
||||
number:
|
||||
type: integer
|
||||
minimum: 0
|
||||
example: 3
|
||||
name:
|
||||
type: string
|
||||
example: Sprinkling System 1
|
||||
controller_id:
|
||||
$ref: '#/components/schemas/controller_id'
|
||||
active_schedule:
|
||||
$ref: '#/components/schemas/schedule-untagged'
|
||||
schedules:
|
||||
type: array
|
||||
maxItems: 7
|
||||
minItems: 7
|
||||
items:
|
||||
$ref: '#/components/schemas/schedule-untagged'
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/tag'
|
||||
is_on:
|
||||
type: boolean
|
||||
description: NULL when unknown
|
||||
schedule-untagged:
|
||||
title: schedule
|
||||
type: object
|
||||
description: ''
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/schedule_id'
|
||||
name:
|
||||
type: string
|
||||
example: Sprinkler Sunny Day
|
||||
periods:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/period'
|
||||
schedule:
|
||||
title: schedule
|
||||
type: object
|
||||
description: ''
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/schedule_id'
|
||||
name:
|
||||
type: string
|
||||
example: Sprinkler Sunny Day
|
||||
periods:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/period'
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/tag'
|
||||
period:
|
||||
title: period
|
||||
type: object
|
||||
properties:
|
||||
start:
|
||||
type: string
|
||||
example: '10:15'
|
||||
format: 24-hour
|
||||
end:
|
||||
type: string
|
||||
format: 24-hour
|
||||
example: '14:45'
|
||||
required:
|
||||
- start
|
||||
- end
|
||||
controller_id:
|
||||
type: string
|
||||
title: controller_id
|
||||
format: uuid
|
||||
example: 589c0eab-a4b4-4f3a-be97-cf03b1dc8edc
|
||||
tag:
|
||||
type: string
|
||||
title: tag
|
||||
example: sprinkler
|
||||
tag_full:
|
||||
title: tag (full)
|
||||
type: object
|
||||
properties:
|
||||
tag:
|
||||
$ref: '#/components/schemas/tag'
|
||||
relays:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/relay'
|
||||
schedules:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
schedule_id:
|
||||
type: string
|
||||
title: schedule_id
|
||||
format: uuid
|
||||
example: 6bceb29b-7d2e-4af3-a26e-11f514dc5cc1
|
||||
macro:
|
||||
title: macro
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: a9a4eab4-6c54-4fe4-b755-bdb2a90b3242
|
||||
name:
|
||||
type: string
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/macro_action'
|
||||
macro_action:
|
||||
title: macro_action
|
||||
type: object
|
||||
description: ''
|
||||
properties:
|
||||
weekday:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 6
|
||||
schedule:
|
||||
$ref: '#/components/schemas/schedule'
|
||||
relay:
|
||||
$ref: '#/components/schemas/relay'
|
||||
required:
|
||||
- weekday
|
||||
- schedule
|
||||
- relay
|
|
@ -1,56 +0,0 @@
|
|||
amends "package://emgauwa.app/pkl/emgauwa@0.1.0#/controller.pkl"
|
||||
|
||||
relays {
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 0
|
||||
inverted = true
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 1
|
||||
inverted = true
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 2
|
||||
inverted = true
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 3
|
||||
inverted = true
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 4
|
||||
inverted = true
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 5
|
||||
inverted = true
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 10
|
||||
inverted = true
|
||||
pulse = 10
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 11
|
||||
inverted = true
|
||||
pulse = 10
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 20
|
||||
inverted = false
|
||||
}
|
||||
new {
|
||||
driver = "null"
|
||||
pin = 21
|
||||
inverted = false
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
amends "package://emgauwa.app/pkl/emgauwa@0.1.0#/core.pkl"
|
|
@ -1,32 +0,0 @@
|
|||
[package]
|
||||
name = "emgauwa-common"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["Tobias Reisinger <tobias@msrg.cc>"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
actix = "0.13"
|
||||
actix-web = "4.4"
|
||||
actix-web-actors = "4.2"
|
||||
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
simple_logger = "4.2"
|
||||
log = "0.4"
|
||||
|
||||
config = "0.13"
|
||||
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros", "chrono"] }
|
||||
libsqlite3-sys = { version = "*", features = ["bundled"] }
|
||||
uuid = "1.6"
|
||||
futures = "0.3"
|
||||
libc = "0.2"
|
||||
|
||||
rppal = "0.17"
|
||||
rppal-pfd = "0.0.5"
|
||||
rppal-mcp23s17 = "0.0.3"
|
|
@ -1,32 +0,0 @@
|
|||
[package]
|
||||
name = "emgauwa-controller"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["Tobias Reisinger <tobias@msrg.cc>"]
|
||||
|
||||
[dependencies]
|
||||
emgauwa-common = { path = "../emgauwa-common" }
|
||||
|
||||
actix = "0.13"
|
||||
|
||||
tokio = { version = "1.34", features = ["io-std", "macros", "rt-multi-thread"] }
|
||||
tokio-tungstenite = "0.21"
|
||||
|
||||
simple_logger = "4.3"
|
||||
log = "0.4"
|
||||
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.5", features = ["serde", "v4"] }
|
||||
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros", "chrono"] }
|
||||
|
||||
futures = "0.3"
|
||||
futures-channel = "0.3"
|
||||
|
||||
rppal = "0.17"
|
||||
rppal-pfd = "0.0.5"
|
||||
rppal-mcp23s17 = "0.0.3"
|
|
@ -1,173 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use actix::{Actor, Context, Handler, Message};
|
||||
use emgauwa_common::constants;
|
||||
use emgauwa_common::drivers::RelayDriver;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::models::Controller;
|
||||
use emgauwa_common::types::RelayStates;
|
||||
use futures::executor::block_on;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::settings::Settings;
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(), EmgauwaError>")]
|
||||
pub struct Reload {}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct UpdateRelayStates {
|
||||
pub relay_states: RelayStates,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(), EmgauwaError>")]
|
||||
pub struct RelayPulse {
|
||||
pub relay_number: i64,
|
||||
pub duration: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Controller")]
|
||||
pub struct GetThis {}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Arc<Notify>")]
|
||||
pub struct GetControllerNotifier {}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Arc<Notify>")]
|
||||
pub struct GetRelayNotifier {}
|
||||
|
||||
pub struct AppState {
|
||||
pub pool: Pool<Sqlite>,
|
||||
pub this: Controller,
|
||||
pub settings: Settings,
|
||||
pub drivers: Vec<Box<dyn RelayDriver>>,
|
||||
pub controller_notifier: Arc<Notify>,
|
||||
pub relay_notifier: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(
|
||||
pool: Pool<Sqlite>,
|
||||
this: Controller,
|
||||
settings: Settings,
|
||||
drivers: Vec<Box<dyn RelayDriver>>,
|
||||
) -> AppState {
|
||||
AppState {
|
||||
pool,
|
||||
this,
|
||||
settings,
|
||||
drivers,
|
||||
controller_notifier: Arc::new(Notify::new()),
|
||||
relay_notifier: Arc::new(Notify::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_controller_change(&self) {
|
||||
self.controller_notifier.notify_one();
|
||||
}
|
||||
|
||||
pub fn notify_relay_change(&self) {
|
||||
self.relay_notifier.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for AppState {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl Handler<Reload> for AppState {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, _msg: Reload, _ctx: &mut Self::Context) -> Self::Result {
|
||||
log::debug!("Reloading controller");
|
||||
let mut pool_conn = block_on(self.pool.acquire())?;
|
||||
|
||||
self.this.reload(&mut pool_conn)?;
|
||||
|
||||
self.notify_controller_change();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<UpdateRelayStates> for AppState {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: UpdateRelayStates, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.this.apply_relay_states(&msg.relay_states);
|
||||
self.drivers
|
||||
.iter_mut()
|
||||
.zip(msg.relay_states.iter())
|
||||
.for_each(|(driver, state)| {
|
||||
if let Err(e) = driver.set(state.unwrap_or(false)) {
|
||||
log::error!("Error setting relay: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
self.notify_relay_change();
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<RelayPulse> for AppState {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, msg: RelayPulse, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let relay_num = msg.relay_number;
|
||||
|
||||
let duration = Duration::from_secs(
|
||||
match msg.duration {
|
||||
None => {
|
||||
self.settings
|
||||
.get_relay(relay_num)
|
||||
.ok_or(EmgauwaError::Other(String::from(
|
||||
"Relay not found in settings",
|
||||
)))?
|
||||
.pulse
|
||||
}
|
||||
Some(dur) => Some(dur as u64),
|
||||
}
|
||||
.unwrap_or(constants::RELAY_PULSE_DURATION),
|
||||
);
|
||||
let now = Instant::now();
|
||||
let until = now + duration;
|
||||
|
||||
self.this.relay_pulse(relay_num, until)?;
|
||||
log::debug!(
|
||||
"Pulsing relay {} for {} seconds until {:?}",
|
||||
relay_num,
|
||||
duration.as_secs(),
|
||||
until
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<GetThis> for AppState {
|
||||
type Result = Controller;
|
||||
|
||||
fn handle(&mut self, _msg: GetThis, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.this.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<GetControllerNotifier> for AppState {
|
||||
type Result = Arc<Notify>;
|
||||
|
||||
fn handle(&mut self, _msg: GetControllerNotifier, _ctx: &mut Self::Context) -> Self::Result {
|
||||
Arc::clone(&self.controller_notifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<GetRelayNotifier> for AppState {
|
||||
type Result = Arc<Notify>;
|
||||
|
||||
fn handle(&mut self, _msg: GetRelayNotifier, _ctx: &mut Self::Context) -> Self::Result {
|
||||
Arc::clone(&self.relay_notifier)
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Driver {
|
||||
Null,
|
||||
Gpio,
|
||||
PiFace,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Driver {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match String::deserialize(deserializer)?.as_str() {
|
||||
"null" => Ok(Driver::Null),
|
||||
"gpio" => Ok(Driver::Gpio),
|
||||
"piface" => Ok(Driver::PiFace),
|
||||
_ => Err(serde::de::Error::custom("invalid driver")),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
use actix::Actor;
|
||||
use emgauwa_common::db;
|
||||
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbSchedule};
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::models::{Controller, FromDbModel};
|
||||
use emgauwa_common::types::EmgauwaUid;
|
||||
use emgauwa_common::utils::{drop_privileges, init_logging};
|
||||
use rppal_pfd::PiFaceDigital;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::Sqlite;
|
||||
|
||||
use crate::relay_loop::run_relays_loop;
|
||||
use crate::settings::Settings;
|
||||
use crate::ws::run_ws_loop;
|
||||
|
||||
mod app_state;
|
||||
mod driver;
|
||||
mod relay_loop;
|
||||
mod settings;
|
||||
mod utils;
|
||||
mod ws;
|
||||
|
||||
async fn create_this_controller(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
settings: &Settings,
|
||||
) -> Result<DbController, EmgauwaError> {
|
||||
DbController::create(
|
||||
conn,
|
||||
&EmgauwaUid::default(),
|
||||
&settings.name,
|
||||
settings.relays.len() as i64,
|
||||
)
|
||||
.await
|
||||
.map_err(EmgauwaError::from)
|
||||
}
|
||||
|
||||
async fn create_this_relay(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
this_controller: &DbController,
|
||||
settings_relay: &settings::Relay,
|
||||
) -> Result<DbRelay, EmgauwaError> {
|
||||
let relay = DbRelay::create(
|
||||
conn,
|
||||
&settings_relay.name,
|
||||
settings_relay.number.ok_or(EmgauwaError::Internal(
|
||||
"Relay number is missing".to_string(),
|
||||
))?,
|
||||
this_controller,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let off = DbSchedule::get_off(conn).await?;
|
||||
for weekday in 0..7 {
|
||||
DbJunctionRelaySchedule::set_schedule(conn, &relay, &off, weekday).await?;
|
||||
}
|
||||
|
||||
Ok(relay)
|
||||
}
|
||||
|
||||
#[actix::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
let settings = settings::init()?;
|
||||
|
||||
drop_privileges(&settings.permissions)?;
|
||||
|
||||
init_logging(&settings.logging.level)?;
|
||||
|
||||
let mut pfd: Option<PiFaceDigital> = None;
|
||||
let drivers = settings.relays_make_drivers(&mut pfd)?;
|
||||
|
||||
let pool = db::init(&settings.database)
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?;
|
||||
|
||||
let mut conn = pool.acquire().await.map_err(EmgauwaError::from)?;
|
||||
|
||||
let db_controller = match DbController::get_all(&mut conn)
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?
|
||||
.pop()
|
||||
{
|
||||
None => futures::executor::block_on(create_this_controller(&mut conn, &settings))?,
|
||||
Some(c) => c,
|
||||
};
|
||||
|
||||
for relay in &settings.relays {
|
||||
if DbRelay::get_by_controller_and_num(
|
||||
&mut conn,
|
||||
&db_controller,
|
||||
relay.number.ok_or(EmgauwaError::Internal(
|
||||
"Relay number is missing".to_string(),
|
||||
))?,
|
||||
)
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?
|
||||
.is_none()
|
||||
{
|
||||
create_this_relay(&mut conn, &db_controller, relay)
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?;
|
||||
}
|
||||
}
|
||||
|
||||
let db_controller = db_controller
|
||||
.update(&mut conn, &db_controller.name, settings.relays.len() as i64)
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?;
|
||||
|
||||
let this = Controller::from_db_model(&mut conn, db_controller).map_err(EmgauwaError::from)?;
|
||||
|
||||
let url = format!(
|
||||
"ws://{}:{}/api/v1/ws/controllers",
|
||||
settings.server.host, settings.server.port
|
||||
);
|
||||
|
||||
let app_state = app_state::AppState::new(pool.clone(), this, settings, drivers).start();
|
||||
|
||||
let _ = tokio::join!(
|
||||
tokio::spawn(run_relays_loop(app_state.clone())),
|
||||
tokio::spawn(run_ws_loop(pool.clone(), app_state.clone(), url)),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use actix::Addr;
|
||||
use chrono::{Local, Timelike};
|
||||
use emgauwa_common::constants::RELAYS_RETRY_TIMEOUT;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::models::Controller;
|
||||
use emgauwa_common::types::{RelayStates, Weekday};
|
||||
use emgauwa_common::utils::printable_relay_states;
|
||||
use futures::pin_mut;
|
||||
use tokio::time;
|
||||
use tokio::time::timeout;
|
||||
use utils::app_state_get_controller_notifier;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::utils;
|
||||
|
||||
pub async fn run_relays_loop(app_state: Addr<AppState>) {
|
||||
log::debug!("Spawned relays loop");
|
||||
loop {
|
||||
let run_result = run_relays(&app_state).await;
|
||||
if let Err(err) = run_result {
|
||||
log::error!("Error running relays: {}", err);
|
||||
}
|
||||
time::sleep(RELAYS_RETRY_TIMEOUT).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_relays(app_state: &Addr<AppState>) -> Result<(), EmgauwaError> {
|
||||
let notifier = &*app_state_get_controller_notifier(app_state).await?;
|
||||
|
||||
let mut last_weekday = emgauwa_common::utils::get_weekday();
|
||||
let mut this = utils::app_state_get_this(app_state).await?;
|
||||
let mut relay_states: RelayStates = Vec::new();
|
||||
init_relay_states(&mut relay_states, &this);
|
||||
calc_relay_states(&mut relay_states, &mut this, app_state).await?;
|
||||
|
||||
let mut duration_override = None;
|
||||
|
||||
loop {
|
||||
log::debug!(
|
||||
"Relay loop at {}: {}",
|
||||
Local::now().naive_local().time(),
|
||||
printable_relay_states(&this.get_relay_states())
|
||||
);
|
||||
|
||||
let notifier_future = notifier.notified();
|
||||
pin_mut!(notifier_future);
|
||||
let mut changed = timeout(
|
||||
get_next_duration(&this, &mut duration_override),
|
||||
&mut notifier_future,
|
||||
)
|
||||
.await
|
||||
.is_ok();
|
||||
|
||||
check_weekday(app_state, &mut last_weekday, &mut changed).await?;
|
||||
|
||||
if changed {
|
||||
log::debug!("Reloading controller in relay loop");
|
||||
this = utils::app_state_get_this(app_state).await?;
|
||||
}
|
||||
|
||||
let now_pulse = Instant::now();
|
||||
duration_override = this
|
||||
.relays
|
||||
.iter_mut()
|
||||
.filter_map(|relay| match relay.check_pulsing(&now_pulse) {
|
||||
None => None,
|
||||
Some(pulse) => {
|
||||
let dur = pulse - now_pulse;
|
||||
log::debug!(
|
||||
"Pulsing relay {} for {}s until {:?} ",
|
||||
relay.r.number,
|
||||
dur.as_secs(),
|
||||
pulse
|
||||
);
|
||||
Some(dur)
|
||||
}
|
||||
})
|
||||
.min();
|
||||
|
||||
calc_relay_states(&mut relay_states, &mut this, app_state).await?;
|
||||
}
|
||||
}
|
||||
|
||||
fn init_relay_states(relay_states: &mut RelayStates, this: &Controller) {
|
||||
relay_states.clear();
|
||||
for _ in 0..this.c.relay_count {
|
||||
relay_states.push(None);
|
||||
}
|
||||
}
|
||||
|
||||
async fn calc_relay_states(
|
||||
relay_states: &mut RelayStates,
|
||||
this: &mut Controller,
|
||||
app_state: &Addr<AppState>,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
let now = Local::now().time();
|
||||
let now_pulse = Instant::now();
|
||||
|
||||
this.relays
|
||||
.iter_mut()
|
||||
.zip(relay_states.iter_mut())
|
||||
.for_each(|(relay, state)| {
|
||||
relay.is_on = Some(
|
||||
relay.active_schedule.is_on(&now) || relay.check_pulsing(&now_pulse).is_some(),
|
||||
);
|
||||
*state = relay.is_on;
|
||||
});
|
||||
utils::app_state_update_relays_on(app_state, relay_states.clone()).await
|
||||
}
|
||||
|
||||
fn get_next_duration(this: &Controller, duration_override: &mut Option<Duration>) -> Duration {
|
||||
if let Some(duration) = duration_override {
|
||||
log::debug!("Duration override. Waiting for {}s", duration.as_secs());
|
||||
return *duration;
|
||||
}
|
||||
|
||||
let now = Local::now().time();
|
||||
let now_in_s = now.num_seconds_from_midnight();
|
||||
let next_timestamp = this
|
||||
.get_next_time(&now)
|
||||
.map_or(86400, |t| t.num_seconds_from_midnight());
|
||||
|
||||
let duration_to_next = Duration::from_secs((next_timestamp - now_in_s) as u64);
|
||||
|
||||
log::debug!(
|
||||
"Next timestamp: {}; Waiting for {}s",
|
||||
next_timestamp,
|
||||
duration_to_next.as_secs()
|
||||
);
|
||||
|
||||
duration_to_next
|
||||
}
|
||||
|
||||
async fn check_weekday(
|
||||
app_state: &Addr<AppState>,
|
||||
last_weekday: &mut Weekday,
|
||||
changed: &mut bool,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
let current_weekday = emgauwa_common::utils::get_weekday();
|
||||
if current_weekday.ne(last_weekday) {
|
||||
log::debug!("Weekday changed");
|
||||
*last_weekday = current_weekday;
|
||||
utils::app_state_reload(app_state).await?;
|
||||
*changed = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::{drivers, settings};
|
||||
use rppal_pfd::PiFaceDigital;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use crate::driver::Driver;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[allow(unused)]
|
||||
pub struct Relay {
|
||||
pub driver: Driver,
|
||||
pub name: String,
|
||||
pub number: Option<i64>,
|
||||
pub pin: u8,
|
||||
pub inverted: bool,
|
||||
pub pulse: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[allow(unused)]
|
||||
pub struct Settings {
|
||||
pub server: settings::Server,
|
||||
pub database: String,
|
||||
pub permissions: settings::Permissions,
|
||||
pub logging: settings::Logging,
|
||||
|
||||
pub name: String,
|
||||
pub relays: Vec<Relay>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
server: settings::Server::default(),
|
||||
database: String::from("sqlite://emgauwa-controller.sqlite"),
|
||||
permissions: settings::Permissions::default(),
|
||||
logging: settings::Logging::default(),
|
||||
|
||||
name: String::from("Emgauwa Controller"),
|
||||
relays: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Relay {
|
||||
fn default() -> Self {
|
||||
Relay {
|
||||
driver: Driver::Gpio,
|
||||
number: None,
|
||||
name: String::from("Relay"),
|
||||
pin: 0,
|
||||
inverted: false,
|
||||
pulse: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> Result<Settings, EmgauwaError> {
|
||||
let mut settings: Settings = settings::load("controller", "CONTROLLER")?;
|
||||
|
||||
for (num, relay) in settings.relays.iter_mut().enumerate() {
|
||||
if relay.number.is_none() {
|
||||
relay.number = Some(num as i64);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn get_relay(&self, number: i64) -> Option<&Relay> {
|
||||
self.relays.iter().find(|r| r.number == Some(number))
|
||||
}
|
||||
|
||||
pub fn relays_make_drivers(
|
||||
&self,
|
||||
pfd: &mut Option<PiFaceDigital>,
|
||||
) -> Result<Vec<Box<dyn drivers::RelayDriver>>, EmgauwaError> {
|
||||
let mut drivers = Vec::new();
|
||||
for relay in &self.relays {
|
||||
drivers.push(relay.make_driver(pfd)?);
|
||||
}
|
||||
Ok(drivers)
|
||||
}
|
||||
}
|
||||
|
||||
impl Relay {
|
||||
pub fn make_driver(
|
||||
&self,
|
||||
pfd: &mut Option<PiFaceDigital>,
|
||||
) -> Result<Box<dyn drivers::RelayDriver>, EmgauwaError> {
|
||||
let driver: Box<dyn drivers::RelayDriver> = match self.driver {
|
||||
Driver::Null => Box::new(drivers::NullDriver::new(self.pin)),
|
||||
Driver::Gpio => Box::new(drivers::GpioDriver::new(self.pin, self.inverted)?),
|
||||
Driver::PiFace => {
|
||||
if pfd.is_none() {
|
||||
*pfd = Some(drivers::PiFaceDriver::init_piface()?);
|
||||
}
|
||||
Box::new(drivers::PiFaceDriver::new(self.pin, pfd)?)
|
||||
}
|
||||
};
|
||||
Ok(driver)
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use actix::Addr;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::models::Controller;
|
||||
use emgauwa_common::types::RelayStates;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
pub async fn app_state_get_this(app_state: &Addr<AppState>) -> Result<Controller, EmgauwaError> {
|
||||
app_state
|
||||
.send(app_state::GetThis {})
|
||||
.await
|
||||
.map_err(EmgauwaError::from)
|
||||
}
|
||||
|
||||
pub async fn app_state_get_relay_notifier(
|
||||
app_state: &Addr<AppState>,
|
||||
) -> Result<Arc<Notify>, EmgauwaError> {
|
||||
app_state
|
||||
.send(app_state::GetRelayNotifier {})
|
||||
.await
|
||||
.map_err(EmgauwaError::from)
|
||||
}
|
||||
|
||||
pub async fn app_state_get_controller_notifier(
|
||||
app_state: &Addr<AppState>,
|
||||
) -> Result<Arc<Notify>, EmgauwaError> {
|
||||
app_state
|
||||
.send(app_state::GetControllerNotifier {})
|
||||
.await
|
||||
.map_err(EmgauwaError::from)
|
||||
}
|
||||
|
||||
pub async fn app_state_reload(app_state: &Addr<AppState>) -> Result<(), EmgauwaError> {
|
||||
app_state
|
||||
.send(app_state::Reload {})
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?
|
||||
}
|
||||
|
||||
pub async fn app_state_update_relays_on(
|
||||
app_state: &Addr<AppState>,
|
||||
relay_states: RelayStates,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
app_state
|
||||
.send(app_state::UpdateRelayStates { relay_states })
|
||||
.await
|
||||
.map_err(EmgauwaError::from)
|
||||
}
|
||||
|
||||
pub async fn app_state_relay_pulse(
|
||||
app_state: &Addr<AppState>,
|
||||
relay_number: i64,
|
||||
duration: Option<u32>,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
app_state
|
||||
.send(app_state::RelayPulse {
|
||||
relay_number,
|
||||
duration,
|
||||
})
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
use actix::Addr;
|
||||
use emgauwa_common::constants::WEBSOCKET_RETRY_TIMEOUT;
|
||||
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbSchedule};
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{Controller, Relay};
|
||||
use emgauwa_common::types::{ControllerWsAction, ScheduleUid};
|
||||
use futures::{future, pin_mut, SinkExt, StreamExt};
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tokio::time;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::{connect_async, tungstenite};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::utils;
|
||||
use crate::utils::{app_state_get_relay_notifier, app_state_get_this};
|
||||
|
||||
pub async fn run_ws_loop(pool: Pool<Sqlite>, app_state: Addr<AppState>, url: String) {
|
||||
log::debug!("Spawned ws loop");
|
||||
loop {
|
||||
let run_result = run_websocket(pool.clone(), &app_state, &url).await;
|
||||
if let Err(err) = run_result {
|
||||
log::error!("Error running websocket: {}", err);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Retrying to connect in {} seconds...",
|
||||
WEBSOCKET_RETRY_TIMEOUT.as_secs()
|
||||
);
|
||||
time::sleep(WEBSOCKET_RETRY_TIMEOUT).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_websocket(
|
||||
pool: Pool<Sqlite>,
|
||||
app_state: &Addr<AppState>,
|
||||
url: &str,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
log::debug!("Trying to connect to {}", url);
|
||||
match connect_async(url).await {
|
||||
Ok(connection) => {
|
||||
log::info!("Websocket connected");
|
||||
let (ws_stream, _) = connection;
|
||||
|
||||
let (mut write, read) = ws_stream.split();
|
||||
|
||||
let ws_action = ControllerWsAction::Register(app_state_get_this(app_state).await?);
|
||||
|
||||
let ws_action_json = serde_json::to_string(&ws_action)?;
|
||||
if let Err(err) = write.send(Message::text(ws_action_json)).await {
|
||||
log::error!("Failed to register at websocket: {}", err);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (app_state_tx, app_state_rx) = futures_channel::mpsc::unbounded::<Message>();
|
||||
tokio::spawn(read_app_state(app_state.clone(), app_state_tx));
|
||||
let app_state_to_ws = app_state_rx.map(Ok).forward(write);
|
||||
|
||||
let read_handler = read.for_each(|msg| handle_message(pool.clone(), app_state, msg));
|
||||
|
||||
pin_mut!(app_state_to_ws, read_handler);
|
||||
future::select(app_state_to_ws, read_handler).await;
|
||||
|
||||
log::warn!("Lost connection to websocket");
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Failed to connect to websocket: {}", err,);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_app_state(
|
||||
app_state: Addr<AppState>,
|
||||
tx: futures_channel::mpsc::UnboundedSender<Message>,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
let notifier = &*app_state_get_relay_notifier(&app_state).await?;
|
||||
loop {
|
||||
notifier.notified().await;
|
||||
log::debug!("Relay change detected");
|
||||
let this = app_state_get_this(&app_state).await?;
|
||||
let relay_states = this.get_relay_states();
|
||||
let ws_action = ControllerWsAction::RelayStates((this.c.uid, relay_states));
|
||||
|
||||
let ws_action_json = serde_json::to_string(&ws_action)?;
|
||||
tx.unbounded_send(Message::text(ws_action_json))
|
||||
.map_err(|_| {
|
||||
EmgauwaError::Other(String::from(
|
||||
"Failed to forward message from app state to websocket",
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
pool: Pool<Sqlite>,
|
||||
app_state: &Addr<AppState>,
|
||||
message_result: Result<Message, tungstenite::Error>,
|
||||
) {
|
||||
let msg = match message_result {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
log::error!("Error reading message: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Message::Text(text) = msg {
|
||||
match serde_json::from_str(&text) {
|
||||
Ok(action) => {
|
||||
log::debug!("Received action: {:?}", action);
|
||||
let mut pool_conn = match pool.acquire().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
log::error!("Failed to acquire database connection: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let action_res = handle_action(&mut pool_conn, app_state, action).await;
|
||||
if let Err(e) = action_res {
|
||||
log::error!("Error handling action: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error deserializing action: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_action(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
app_state: &Addr<AppState>,
|
||||
action: ControllerWsAction,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
let this = app_state_get_this(app_state).await?;
|
||||
|
||||
match action {
|
||||
ControllerWsAction::Controller(controller) => {
|
||||
handle_controller(conn, &this, controller).await?
|
||||
}
|
||||
ControllerWsAction::Relays(relays) => handle_relays(conn, &this, relays).await?,
|
||||
ControllerWsAction::Schedules(schedules) => handle_schedules(conn, schedules).await?,
|
||||
ControllerWsAction::RelayPulse((relay_num, duration)) => {
|
||||
handle_relay_pulse(app_state, relay_num, duration).await?
|
||||
}
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
utils::app_state_reload(app_state).await
|
||||
}
|
||||
|
||||
async fn handle_controller(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
this: &Controller,
|
||||
controller: Controller,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
if controller.c.uid != this.c.uid {
|
||||
return Err(EmgauwaError::Other(String::from(
|
||||
"Controller UID mismatch during update",
|
||||
)));
|
||||
}
|
||||
DbController::get_by_uid(conn, &controller.c.uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?
|
||||
.update(conn, controller.c.name.as_str(), this.c.relay_count)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_schedules(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
schedules: Vec<DbSchedule>,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
let mut handled_uids = vec![
|
||||
// on and off schedules are always present and should not be updated
|
||||
ScheduleUid::On,
|
||||
ScheduleUid::Off,
|
||||
];
|
||||
for schedule in schedules {
|
||||
if handled_uids.contains(&schedule.uid) {
|
||||
continue;
|
||||
}
|
||||
handled_uids.push(schedule.uid.clone());
|
||||
|
||||
log::debug!("Handling schedule: {:?}", schedule);
|
||||
let schedule_db = DbSchedule::get_by_uid(conn, &schedule.uid).await?;
|
||||
|
||||
if let Some(schedule_db) = schedule_db {
|
||||
schedule_db
|
||||
.update(conn, schedule.name.as_str(), &schedule.periods)
|
||||
.await?;
|
||||
} else {
|
||||
DbSchedule::create(
|
||||
conn,
|
||||
schedule.uid.clone(),
|
||||
schedule.name.as_str(),
|
||||
&schedule.periods,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_relays(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
this: &Controller,
|
||||
relays: Vec<Relay>,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
for relay in relays {
|
||||
if relay.controller.uid != this.c.uid {
|
||||
return Err(EmgauwaError::Other(String::from(
|
||||
"Controller UID mismatch during relay update",
|
||||
)));
|
||||
}
|
||||
let db_relay = DbRelay::get_by_controller_and_num(conn, &this.c, relay.r.number)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
db_relay.update(conn, relay.r.name.as_str()).await?;
|
||||
|
||||
handle_schedules(conn, relay.schedules.clone()).await?;
|
||||
|
||||
let mut schedules = Vec::new(); // We need to get the schedules from the database to have the right IDs
|
||||
for schedule in relay.schedules {
|
||||
schedules.push(
|
||||
DbSchedule::get_by_uid(conn, &schedule.uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?,
|
||||
);
|
||||
}
|
||||
|
||||
DbJunctionRelaySchedule::set_schedules(conn, &db_relay, schedules.iter().collect()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_relay_pulse(
|
||||
app_state: &Addr<AppState>,
|
||||
relay_num: i64,
|
||||
duration: Option<u32>,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
utils::app_state_relay_pulse(app_state, relay_num, duration).await
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
[package]
|
||||
name = "emgauwa-core"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["Tobias Reisinger <tobias@msrg.cc>"]
|
||||
|
||||
[dependencies]
|
||||
emgauwa-common = { path = "../emgauwa-common" }
|
||||
|
||||
actix = "0.13"
|
||||
actix-web = "4.4"
|
||||
actix-web-actors = "4.2"
|
||||
actix-cors = "0.7"
|
||||
|
||||
utoipa = "4.2"
|
||||
utoipa-swagger-ui = { version = "6.0", features = ["actix-web", "debug-embed"] }
|
||||
|
||||
log = "0.4"
|
||||
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.5", features = ["serde", "v4"] }
|
||||
itertools = "0.12"
|
||||
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros", "chrono"] }
|
||||
|
||||
futures = "0.3"
|
||||
tokio = { version = "1.36", features = ["rt", "rt-multi-thread"] }
|
|
@ -1,15 +0,0 @@
|
|||
use std::process::{exit, Command};
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=../api.v1.yaml");
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("yq . < ../api.v1.yaml > $OUT_DIR/api.v1.json")
|
||||
.output()
|
||||
.expect("Failed to convert api documentation to json");
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!("Error: {}", String::from_utf8_lossy(&output.stderr));
|
||||
exit(1);
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use actix::{Actor, Addr, Context, Handler, Message, Recipient};
|
||||
use emgauwa_common::db::DbController;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::models::{convert_db_list, Controller, Relay};
|
||||
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RelayStates};
|
||||
use futures::executor::block_on;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::handlers::v1::ws::relays::{RelaysWs, SendRelays};
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(), EmgauwaError>")]
|
||||
pub struct DisconnectController {
|
||||
pub controller_uid: EmgauwaUid,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(), EmgauwaError>")]
|
||||
pub struct ConnectController {
|
||||
pub address: Recipient<ControllerWsAction>,
|
||||
pub controller: Controller,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct UpdateRelayStates {
|
||||
pub controller_uid: EmgauwaUid,
|
||||
pub relay_states: RelayStates,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<Vec<Relay>, EmgauwaError>")]
|
||||
pub struct GetRelays {}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "Result<(), EmgauwaError>")]
|
||||
pub struct Action {
|
||||
pub controller_uid: EmgauwaUid,
|
||||
pub action: ControllerWsAction,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct ConnectRelayClient {
|
||||
pub addr: Addr<RelaysWs>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub pool: Pool<Sqlite>,
|
||||
pub connected_controllers: HashMap<EmgauwaUid, (Controller, Recipient<ControllerWsAction>)>,
|
||||
pub connected_relay_clients: Vec<Addr<RelaysWs>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(pool: Pool<Sqlite>) -> AppState {
|
||||
AppState {
|
||||
pool,
|
||||
connected_controllers: HashMap::new(),
|
||||
connected_relay_clients: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_relays(&self) -> Result<Vec<Relay>, EmgauwaError> {
|
||||
let mut pool_conn = block_on(self.pool.acquire())?;
|
||||
let db_controllers = block_on(DbController::get_all(&mut pool_conn))?;
|
||||
let mut controllers: Vec<Controller> = convert_db_list(&mut pool_conn, db_controllers)?;
|
||||
|
||||
self.connected_controllers
|
||||
.iter()
|
||||
.for_each(|(uid, (connected_controller, _))| {
|
||||
if let Some(c) = controllers.iter_mut().find(|c| c.c.uid == *uid) {
|
||||
c.apply_relay_states(&connected_controller.get_relay_states());
|
||||
}
|
||||
});
|
||||
|
||||
let mut relays: Vec<Relay> = Vec::new();
|
||||
controllers.iter().for_each(|c| {
|
||||
relays.extend(c.relays.clone());
|
||||
});
|
||||
|
||||
Ok(relays)
|
||||
}
|
||||
|
||||
fn notify_relay_clients(&mut self) {
|
||||
self.connected_relay_clients.retain(|addr| addr.connected());
|
||||
|
||||
match self.get_relays() {
|
||||
Ok(relays) => match serde_json::to_string(&relays) {
|
||||
Ok(json) => {
|
||||
self.connected_relay_clients.iter_mut().for_each(|addr| {
|
||||
let relays_json = json.clone();
|
||||
addr.do_send(SendRelays { relays_json });
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to serialize relays: {:?}", err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("Failed to get relays: {:?}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for AppState {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl Handler<DisconnectController> for AppState {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, msg: DisconnectController, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut pool_conn = block_on(self.pool.acquire())?;
|
||||
|
||||
if let Some((controller, address)) = self.connected_controllers.remove(&msg.controller_uid)
|
||||
{
|
||||
if let Err(err) = block_on(controller.c.update_active(&mut pool_conn, false)) {
|
||||
log::error!(
|
||||
"Failed to mark controller {} as inactive: {:?}",
|
||||
controller.c.uid,
|
||||
err
|
||||
);
|
||||
}
|
||||
// TODO: why does the block_on(send()) version not return? The AppState will be stuck.
|
||||
//block_on(address.send(ControllerWsAction::Disconnect))??;
|
||||
address.do_send(ControllerWsAction::Disconnect);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ConnectController> for AppState {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, msg: ConnectController, _ctx: &mut Self::Context) -> Self::Result {
|
||||
log::debug!("Connecting controller: {}", msg.controller.c.uid);
|
||||
self.connected_controllers
|
||||
.insert(msg.controller.c.uid.clone(), (msg.controller, msg.address));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<UpdateRelayStates> for AppState {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: UpdateRelayStates, _ctx: &mut Self::Context) -> Self::Result {
|
||||
if let Some((controller, _)) = self.connected_controllers.get_mut(&msg.controller_uid) {
|
||||
controller.apply_relay_states(&msg.relay_states);
|
||||
}
|
||||
self.notify_relay_clients();
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<GetRelays> for AppState {
|
||||
type Result = Result<Vec<Relay>, EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, _msg: GetRelays, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.get_relays()
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<Action> for AppState {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, msg: Action, _ctx: &mut Self::Context) -> Self::Result {
|
||||
log::debug!("Forwarding action: {:?}", msg.action);
|
||||
if let Some((_, address)) = self.connected_controllers.get(&msg.controller_uid) {
|
||||
// TODO: why does the block_on(send()) version not return? The AppState will be stuck.
|
||||
//block_on(address.send(msg.action))?
|
||||
address.do_send(msg.action);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(EmgauwaError::Connection(msg.controller_uid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ConnectRelayClient> for AppState {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ConnectRelayClient, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.connected_relay_clients.push(msg.addr);
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
use actix_web::{error, Error, HttpRequest, HttpResponse};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
pub mod v1;
|
||||
|
||||
enum EmgauwaJsonPayLoadError {
|
||||
Error(error::JsonPayloadError),
|
||||
}
|
||||
|
||||
impl Serialize for EmgauwaJsonPayLoadError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_struct("error", 3)?;
|
||||
s.serialize_field("type", "json-payload-error")?;
|
||||
s.serialize_field("code", &400)?;
|
||||
s.serialize_field(
|
||||
"description",
|
||||
&match self {
|
||||
EmgauwaJsonPayLoadError::Error(err) => format!("{}", err),
|
||||
},
|
||||
)?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_error_handler(err: error::JsonPayloadError, _: &HttpRequest) -> Error {
|
||||
error::InternalError::from_response(
|
||||
"",
|
||||
HttpResponse::BadRequest()
|
||||
.content_type("application/json")
|
||||
.json(EmgauwaJsonPayLoadError::Error(err)),
|
||||
)
|
||||
.into()
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
use actix::Addr;
|
||||
use actix_web::{delete, get, put, web, HttpResponse};
|
||||
use emgauwa_common::db::DbController;
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{convert_db_list, Controller, FromDbModel};
|
||||
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RequestControllerUpdate};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[get("/controllers")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_controllers = DbController::get_all(&mut pool_conn).await?;
|
||||
|
||||
let controllers: Vec<Controller> = convert_db_list(&mut pool_conn, db_controllers)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(controllers))
|
||||
}
|
||||
|
||||
#[get("/controllers/{controller_id}")]
|
||||
pub async fn show(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_controller = Controller::from_db_model(&mut pool_conn, controller)?;
|
||||
Ok(HttpResponse::Ok().json(return_controller))
|
||||
}
|
||||
|
||||
#[put("/controllers/{controller_id}")]
|
||||
pub async fn update(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String,)>,
|
||||
data: web::Json<RequestControllerUpdate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let controller = controller
|
||||
.update(&mut pool_conn, data.name.as_str(), controller.relay_count)
|
||||
.await?;
|
||||
|
||||
let return_controller = Controller::from_db_model(&mut pool_conn, controller)?;
|
||||
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid: uid.clone(),
|
||||
action: ControllerWsAction::Controller(return_controller.clone()),
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(HttpResponse::Ok().json(return_controller))
|
||||
}
|
||||
|
||||
#[delete("/controllers/{controller_id}")]
|
||||
pub async fn delete(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
app_state
|
||||
.send(app_state::DisconnectController {
|
||||
controller_uid: uid.clone(),
|
||||
})
|
||||
.await??;
|
||||
|
||||
DbController::delete_by_uid(&mut pool_conn, uid).await?;
|
||||
Ok(HttpResponse::Ok().json("controller got deleted"))
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
use actix::Addr;
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse};
|
||||
use emgauwa_common::db::DbMacro;
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{convert_db_list, FromDbModel, Macro, MacroAction, Relay};
|
||||
use emgauwa_common::types::{
|
||||
ControllerWsAction, EmgauwaUid, RequestMacroCreate, RequestMacroExecute, RequestMacroUpdate,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[get("/macros")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_macros = DbMacro::get_all(&mut pool_conn).await?;
|
||||
let macros: Vec<Macro> = convert_db_list(&mut pool_conn, db_macros)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(macros))
|
||||
}
|
||||
|
||||
#[get("/macros/{macro_id}")]
|
||||
pub async fn show(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (macro_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
|
||||
|
||||
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_macro = Macro::from_db_model(&mut pool_conn, db_macro)?;
|
||||
Ok(HttpResponse::Ok().json(return_macro))
|
||||
}
|
||||
|
||||
#[post("/macros")]
|
||||
pub async fn add(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<RequestMacroCreate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let new_macro = DbMacro::create(&mut pool_conn, EmgauwaUid::default(), &data.name).await?;
|
||||
|
||||
new_macro
|
||||
.set_actions(&mut pool_conn, data.actions.as_slice())
|
||||
.await?;
|
||||
|
||||
let return_macro = Macro::from_db_model(&mut pool_conn, new_macro)?;
|
||||
Ok(HttpResponse::Created().json(return_macro))
|
||||
}
|
||||
|
||||
#[put("/macros/{macro_id}")]
|
||||
pub async fn update(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
data: web::Json<RequestMacroUpdate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (macro_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
|
||||
|
||||
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
if let Some(name) = &data.name {
|
||||
db_macro.update(&mut pool_conn, name).await?;
|
||||
}
|
||||
|
||||
if let Some(actions) = &data.actions {
|
||||
db_macro
|
||||
.set_actions(&mut pool_conn, actions.as_slice())
|
||||
.await?;
|
||||
}
|
||||
|
||||
let return_macro = Macro::from_db_model(&mut pool_conn, db_macro)?;
|
||||
Ok(HttpResponse::Ok().json(return_macro))
|
||||
}
|
||||
|
||||
#[delete("/macros/{macro_id}")]
|
||||
pub async fn delete(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (macro_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
|
||||
|
||||
DbMacro::delete_by_uid(&mut pool_conn, uid).await?;
|
||||
Ok(HttpResponse::Ok().json("macro got deleted"))
|
||||
}
|
||||
|
||||
#[put("/macros/{macro_id}/execute")]
|
||||
pub async fn execute(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String,)>,
|
||||
query: web::Query<RequestMacroExecute>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (macro_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(macro_uid.as_str())?;
|
||||
|
||||
let db_macro = DbMacro::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let actions_db = match query.weekday {
|
||||
None => db_macro.get_actions(&mut pool_conn).await?,
|
||||
Some(weekday) => {
|
||||
db_macro
|
||||
.get_actions_weekday(&mut pool_conn, weekday)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
let mut actions: Vec<MacroAction> = convert_db_list(&mut pool_conn, actions_db)?;
|
||||
|
||||
for action in &actions {
|
||||
action.execute(&mut pool_conn).await?;
|
||||
}
|
||||
|
||||
let affected_controller_uids: Vec<EmgauwaUid> = actions
|
||||
.iter()
|
||||
.map(|action| action.relay.controller_id.clone())
|
||||
.unique()
|
||||
.collect();
|
||||
|
||||
for controller_uid in affected_controller_uids {
|
||||
let mut affected_relays: Vec<Relay> = Vec::new();
|
||||
let mut affected_relay_ids: Vec<i64> = Vec::new();
|
||||
|
||||
for action in actions.iter_mut() {
|
||||
if affected_relay_ids.contains(&action.relay.r.id) {
|
||||
continue;
|
||||
}
|
||||
action.relay.reload(&mut pool_conn)?;
|
||||
affected_relays.push(action.relay.clone());
|
||||
affected_relay_ids.push(action.relay.r.id);
|
||||
}
|
||||
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid,
|
||||
action: ControllerWsAction::Relays(affected_relays.clone()),
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().finish()) // TODO add a message?
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
pub mod controllers;
|
||||
pub mod macros;
|
||||
pub mod relays;
|
||||
pub mod schedules;
|
||||
pub mod tags;
|
||||
pub mod ws;
|
|
@ -1,185 +0,0 @@
|
|||
use actix::Addr;
|
||||
use actix_web::{get, post, put, web, HttpResponse};
|
||||
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbTag};
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{convert_db_list, FromDbModel, Relay};
|
||||
use emgauwa_common::types::{
|
||||
ControllerWsAction, EmgauwaUid, RequestRelayPulse, RequestRelayUpdate,
|
||||
};
|
||||
use emgauwa_common::utils;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[get("/relays")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_relays = DbRelay::get_all(&mut pool_conn).await?;
|
||||
|
||||
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(relays))
|
||||
}
|
||||
|
||||
#[get("/relays/tag/{tag}")]
|
||||
pub async fn tagged(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (tag,) = path.into_inner();
|
||||
let tag_db = DbTag::get_by_tag(&mut pool_conn, &tag)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let db_relays = DbRelay::get_by_tag(&mut pool_conn, &tag_db).await?;
|
||||
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(relays))
|
||||
}
|
||||
|
||||
#[get("/controllers/{controller_id}/relays")]
|
||||
pub async fn index_for_controller(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid,) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let db_relays = controller.get_relays(&mut pool_conn).await?;
|
||||
|
||||
let relays: Vec<Relay> = convert_db_list(&mut pool_conn, db_relays)?;
|
||||
Ok(HttpResponse::Ok().json(relays))
|
||||
}
|
||||
|
||||
#[get("/controllers/{controller_id}/relays/{relay_num}")]
|
||||
pub async fn show_for_controller(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String, i64)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid, relay_num) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_relay = Relay::from_db_model(&mut pool_conn, relay)?;
|
||||
Ok(HttpResponse::Ok().json(return_relay))
|
||||
}
|
||||
|
||||
#[put("/controllers/{controller_id}/relays/{relay_num}")]
|
||||
pub async fn update_for_controller(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String, i64)>,
|
||||
data: web::Json<RequestRelayUpdate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid, relay_num) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let mut relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
if let Some(name) = &data.name {
|
||||
relay = relay.update(&mut pool_conn, name.as_str()).await?;
|
||||
}
|
||||
|
||||
if let Some(schedule_uids) = &data.schedules {
|
||||
if schedule_uids.len() == 7 {
|
||||
let mut schedules = Vec::new();
|
||||
for s_uid in schedule_uids {
|
||||
schedules.push(s_uid.get_schedule(&mut pool_conn).await?);
|
||||
}
|
||||
|
||||
DbJunctionRelaySchedule::set_schedules(
|
||||
&mut pool_conn,
|
||||
&relay,
|
||||
schedules.iter().collect(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(s_uid) = &data.active_schedule {
|
||||
let schedule = s_uid.get_schedule(&mut pool_conn).await?;
|
||||
DbJunctionRelaySchedule::set_schedule(
|
||||
&mut pool_conn,
|
||||
&relay,
|
||||
&schedule,
|
||||
utils::get_weekday(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(tags) = &data.tags {
|
||||
relay.set_tags(&mut pool_conn, tags.as_slice()).await?;
|
||||
}
|
||||
|
||||
let relay = relay.reload(&mut pool_conn).await?;
|
||||
|
||||
let return_relay = Relay::from_db_model(&mut pool_conn, relay)?;
|
||||
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid: uid,
|
||||
action: ControllerWsAction::Relays(vec![return_relay.clone()]),
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(HttpResponse::Ok().json(return_relay))
|
||||
}
|
||||
|
||||
#[post("/controllers/{controller_id}/relays/{relay_num}/pulse")]
|
||||
pub async fn pulse(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String, i64)>,
|
||||
data: web::Json<RequestRelayPulse>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (controller_uid, relay_num) = path.into_inner();
|
||||
let uid = EmgauwaUid::try_from(controller_uid.as_str())?;
|
||||
|
||||
let controller = DbController::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let relay = DbRelay::get_by_controller_and_num(&mut pool_conn, &controller, relay_num)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let duration = data.duration.filter(|&d| d > 0);
|
||||
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid: uid,
|
||||
action: ControllerWsAction::RelayPulse((relay.number, duration)),
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(HttpResponse::Ok().finish()) // TODO add a message?
|
||||
}
|
|
@ -1,197 +0,0 @@
|
|||
use actix::Addr;
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse};
|
||||
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbSchedule, DbTag};
|
||||
use emgauwa_common::errors::{ApiError, DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{convert_db_list, FromDbModel, Schedule};
|
||||
use emgauwa_common::types::{
|
||||
ControllerWsAction, RequestScheduleCreate, RequestScheduleUpdate, ScheduleUid,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state;
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[get("/schedules")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_schedules = DbSchedule::get_all(&mut pool_conn).await?;
|
||||
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(schedules))
|
||||
}
|
||||
|
||||
#[get("/schedules/tag/{tag}")]
|
||||
pub async fn tagged(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (tag,) = path.into_inner();
|
||||
let tag_db = DbTag::get_by_tag(&mut pool_conn, &tag)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let db_schedules = DbSchedule::get_by_tag(&mut pool_conn, &tag_db).await?;
|
||||
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(schedules))
|
||||
}
|
||||
|
||||
#[get("/schedules/{schedule_id}")]
|
||||
pub async fn show(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (schedule_uid,) = path.into_inner();
|
||||
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
|
||||
|
||||
let schedule = DbSchedule::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_schedule = Schedule::from_db_model(&mut pool_conn, schedule)?;
|
||||
Ok(HttpResponse::Ok().json(return_schedule))
|
||||
}
|
||||
|
||||
#[post("/schedules")]
|
||||
pub async fn add(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<RequestScheduleCreate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let new_schedule = DbSchedule::create(
|
||||
&mut pool_conn,
|
||||
ScheduleUid::default(),
|
||||
&data.name,
|
||||
&data.periods,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(tags) = &data.tags {
|
||||
new_schedule
|
||||
.set_tags(&mut pool_conn, tags.as_slice())
|
||||
.await?;
|
||||
}
|
||||
|
||||
let return_schedule = Schedule::from_db_model(&mut pool_conn, new_schedule)?;
|
||||
Ok(HttpResponse::Created().json(return_schedule))
|
||||
}
|
||||
|
||||
async fn add_list_single(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
request_schedule: &RequestScheduleCreate,
|
||||
) -> Result<DbSchedule, DatabaseError> {
|
||||
let new_schedule = DbSchedule::create(
|
||||
conn,
|
||||
ScheduleUid::default(),
|
||||
&request_schedule.name,
|
||||
&request_schedule.periods,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(tags) = &request_schedule.tags {
|
||||
new_schedule.set_tags(conn, tags.as_slice()).await?;
|
||||
}
|
||||
|
||||
Ok(new_schedule)
|
||||
}
|
||||
|
||||
#[post("/schedules/list")]
|
||||
pub async fn add_list(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<Vec<RequestScheduleCreate>>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let mut db_schedules: Vec<DbSchedule> = Vec::new();
|
||||
for s in data.iter() {
|
||||
let new_s = futures::executor::block_on(add_list_single(&mut pool_conn, s))?;
|
||||
db_schedules.push(new_s);
|
||||
}
|
||||
|
||||
let schedules: Vec<Schedule> = convert_db_list(&mut pool_conn, db_schedules)?;
|
||||
Ok(HttpResponse::Created().json(schedules))
|
||||
}
|
||||
|
||||
#[put("/schedules/{schedule_id}")]
|
||||
pub async fn update(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
path: web::Path<(String,)>,
|
||||
data: web::Json<RequestScheduleUpdate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (schedule_uid,) = path.into_inner();
|
||||
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
|
||||
|
||||
let schedule = DbSchedule::get_by_uid(&mut pool_conn, &uid)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let name = match &data.name {
|
||||
None => schedule.name.as_str(),
|
||||
Some(name) => name.as_str(),
|
||||
};
|
||||
|
||||
let periods = match &data.periods {
|
||||
None => &schedule.periods,
|
||||
Some(period) => period,
|
||||
};
|
||||
|
||||
let schedule = schedule.update(&mut pool_conn, name, periods).await?;
|
||||
|
||||
if let Some(tags) = &data.tags {
|
||||
schedule.set_tags(&mut pool_conn, tags.as_slice()).await?;
|
||||
}
|
||||
|
||||
let controller_ids: Vec<i64> = DbJunctionRelaySchedule::get_relays(&mut pool_conn, &schedule)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| r.controller_id)
|
||||
.unique()
|
||||
.collect();
|
||||
|
||||
for controller_id in controller_ids {
|
||||
let controller = DbController::get(&mut pool_conn, controller_id)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
app_state
|
||||
.send(app_state::Action {
|
||||
controller_uid: controller.uid,
|
||||
action: ControllerWsAction::Schedules(vec![schedule.clone()]),
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
|
||||
|
||||
let return_schedule = Schedule::from_db_model(&mut pool_conn, schedule)?;
|
||||
Ok(HttpResponse::Ok().json(return_schedule))
|
||||
}
|
||||
|
||||
#[delete("/schedules/{schedule_id}")]
|
||||
pub async fn delete(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (schedule_uid,) = path.into_inner();
|
||||
let uid = ScheduleUid::try_from(schedule_uid.as_str())?;
|
||||
|
||||
match uid {
|
||||
ScheduleUid::Off => Err(EmgauwaError::from(ApiError::ProtectedSchedule)),
|
||||
ScheduleUid::On => Err(EmgauwaError::from(ApiError::ProtectedSchedule)),
|
||||
ScheduleUid::Any(_) => {
|
||||
DbSchedule::delete_by_uid(&mut pool_conn, uid).await?;
|
||||
Ok(HttpResponse::Ok().json("schedule got deleted"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
use actix_web::{delete, get, post, web, HttpResponse};
|
||||
use emgauwa_common::db::DbTag;
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{FromDbModel, Tag};
|
||||
use emgauwa_common::types::RequestTagCreate;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
#[get("/tags")]
|
||||
pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let db_tags = DbTag::get_all(&mut pool_conn).await?;
|
||||
|
||||
let tags: Vec<String> = db_tags.iter().map(|t| t.tag.clone()).collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(tags))
|
||||
}
|
||||
|
||||
#[get("/tags/{tag_name}")]
|
||||
pub async fn show(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (tag_name,) = path.into_inner();
|
||||
|
||||
let tag = DbTag::get_by_tag(&mut pool_conn, &tag_name)
|
||||
.await?
|
||||
.ok_or(DatabaseError::NotFound)?;
|
||||
|
||||
let return_tag = Tag::from_db_model(&mut pool_conn, tag)?;
|
||||
Ok(HttpResponse::Ok().json(return_tag))
|
||||
}
|
||||
|
||||
#[delete("/tags/{tag_name}")]
|
||||
pub async fn delete(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
path: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let (tag_name,) = path.into_inner();
|
||||
|
||||
DbTag::delete_by_tag(&mut pool_conn, &tag_name).await?;
|
||||
Ok(HttpResponse::Ok().json("tag got deleted"))
|
||||
}
|
||||
|
||||
#[post("/tags")]
|
||||
pub async fn add(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<RequestTagCreate>,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let mut pool_conn = pool.acquire().await?;
|
||||
|
||||
let new_tag = DbTag::create(&mut pool_conn, &data.tag).await?;
|
||||
|
||||
let cache = (Vec::new(), Vec::new()); // a new tag can't have any relays or schedules
|
||||
let return_tag = Tag::from_db_model_cache(&mut pool_conn, new_tag, cache)?;
|
||||
Ok(HttpResponse::Created().json(return_tag))
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
use actix::{Actor, AsyncContext};
|
||||
use emgauwa_common::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbSchedule};
|
||||
use emgauwa_common::errors::{DatabaseError, EmgauwaError};
|
||||
use emgauwa_common::models::{Controller, FromDbModel};
|
||||
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid, RelayStates};
|
||||
use emgauwa_common::utils;
|
||||
use futures::executor::block_on;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::Sqlite;
|
||||
|
||||
use crate::app_state::{Action, ConnectController, UpdateRelayStates};
|
||||
use crate::handlers::v1::ws::controllers::ControllersWs;
|
||||
|
||||
impl ControllersWs {
|
||||
pub fn handle_register(
|
||||
&mut self,
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
ctx: &mut <ControllersWs as Actor>::Context,
|
||||
controller: Controller,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
log::info!(
|
||||
"Registering controller: {} ({})",
|
||||
controller.c.name,
|
||||
controller.c.uid
|
||||
);
|
||||
let c = &controller.c;
|
||||
let controller_db = block_on(DbController::get_by_uid_or_create(
|
||||
conn,
|
||||
&c.uid,
|
||||
&c.name,
|
||||
c.relay_count,
|
||||
))?;
|
||||
block_on(controller_db.update_active(conn, true))?;
|
||||
// update only the relay count
|
||||
block_on(controller_db.update(conn, &controller_db.name, c.relay_count))?;
|
||||
|
||||
for relay in &controller.relays {
|
||||
log::debug!(
|
||||
"Registering relay: {} ({})",
|
||||
relay.r.name,
|
||||
match relay.is_on {
|
||||
Some(true) => "+",
|
||||
Some(false) => "-",
|
||||
None => "?",
|
||||
}
|
||||
);
|
||||
let (new_relay, created) = block_on(DbRelay::get_by_controller_and_num_or_create(
|
||||
conn,
|
||||
&controller_db,
|
||||
relay.r.number,
|
||||
&relay.r.name,
|
||||
))?;
|
||||
if created {
|
||||
let mut relay_schedules = Vec::new();
|
||||
for schedule in &relay.schedules {
|
||||
let (new_schedule, _) = block_on(DbSchedule::get_by_uid_or_create(
|
||||
conn,
|
||||
schedule.uid.clone(),
|
||||
&schedule.name,
|
||||
&schedule.periods,
|
||||
))?;
|
||||
relay_schedules.push(new_schedule);
|
||||
}
|
||||
|
||||
block_on(DbJunctionRelaySchedule::set_schedules(
|
||||
conn,
|
||||
&new_relay,
|
||||
relay_schedules.iter().collect(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
let controller_uid = &controller.c.uid;
|
||||
let controller_db = block_on(DbController::get_by_uid(conn, controller_uid))?
|
||||
.ok_or(DatabaseError::InsertGetError)?;
|
||||
let controller = Controller::from_db_model(conn, controller_db)?;
|
||||
|
||||
let addr = ctx.address();
|
||||
self.controller_uid = Some(controller_uid.clone());
|
||||
block_on(self.app_state.send(ConnectController {
|
||||
address: addr.recipient(),
|
||||
controller: controller.clone(),
|
||||
}))??;
|
||||
|
||||
block_on(self.app_state.send(Action {
|
||||
controller_uid: controller_uid.clone(),
|
||||
action: ControllerWsAction::Controller(controller.clone()),
|
||||
}))??;
|
||||
block_on(self.app_state.send(Action {
|
||||
controller_uid: controller_uid.clone(),
|
||||
action: ControllerWsAction::Relays(controller.relays),
|
||||
}))??;
|
||||
|
||||
log::debug!("Done registering controller");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_relay_states(
|
||||
&mut self,
|
||||
controller_uid: EmgauwaUid,
|
||||
relay_states: RelayStates,
|
||||
) -> Result<(), EmgauwaError> {
|
||||
log::debug!(
|
||||
"Received relay states: {} for {}",
|
||||
utils::printable_relay_states(&relay_states),
|
||||
controller_uid
|
||||
);
|
||||
block_on(self.app_state.send(UpdateRelayStates {
|
||||
controller_uid,
|
||||
relay_states,
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
mod handlers;
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler};
|
||||
use actix_web_actors::ws;
|
||||
use actix_web_actors::ws::ProtocolError;
|
||||
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::types::{ControllerWsAction, EmgauwaUid};
|
||||
use futures::executor::block_on;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use ws::Message;
|
||||
|
||||
use crate::app_state::{AppState, DisconnectController};
|
||||
use crate::utils::flatten_result;
|
||||
|
||||
pub struct ControllersWs {
|
||||
pub pool: Pool<Sqlite>,
|
||||
pub controller_uid: Option<EmgauwaUid>,
|
||||
pub app_state: Addr<AppState>,
|
||||
pub hb: Instant,
|
||||
}
|
||||
|
||||
impl Actor for ControllersWs {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
self.hb(ctx);
|
||||
}
|
||||
|
||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||
if let Some(controller_uid) = &self.controller_uid {
|
||||
let flat_res = flatten_result(
|
||||
block_on(self.app_state.send(DisconnectController {
|
||||
controller_uid: controller_uid.clone(),
|
||||
}))
|
||||
.map_err(EmgauwaError::from),
|
||||
);
|
||||
if let Err(err) = flat_res {
|
||||
log::error!("Error disconnecting controller: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControllersWs {
|
||||
pub fn handle_action(
|
||||
&mut self,
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
ctx: &mut <ControllersWs as Actor>::Context,
|
||||
action: ControllerWsAction,
|
||||
) {
|
||||
let action_res = match action {
|
||||
ControllerWsAction::Register(controller) => self.handle_register(conn, ctx, controller),
|
||||
ControllerWsAction::RelayStates((controller_uid, relay_states)) => {
|
||||
self.handle_relay_states(controller_uid, relay_states)
|
||||
}
|
||||
_ => Ok(()),
|
||||
};
|
||||
if let Err(e) = action_res {
|
||||
log::error!("Error handling action: {:?}", e);
|
||||
ctx.text(
|
||||
serde_json::to_string(&e).unwrap_or(format!("Error in handling action: {:?}", e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// helper method that sends ping to client every 5 seconds (HEARTBEAT_INTERVAL).
|
||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
// check client heartbeats
|
||||
if Instant::now().duration_since(act.hb) > HEARTBEAT_TIMEOUT {
|
||||
log::warn!("Websocket Controller heartbeat failed, disconnecting!");
|
||||
ctx.stop();
|
||||
// don't try to send a ping
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping(&[]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ControllerWsAction> for ControllersWs {
|
||||
type Result = Result<(), EmgauwaError>;
|
||||
|
||||
fn handle(&mut self, action: ControllerWsAction, ctx: &mut Self::Context) -> Self::Result {
|
||||
match action {
|
||||
ControllerWsAction::Disconnect => {
|
||||
ctx.close(None);
|
||||
ctx.stop();
|
||||
}
|
||||
_ => {
|
||||
let action_json = serde_json::to_string(&action)?;
|
||||
ctx.text(action_json);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<Message, ProtocolError>> for ControllersWs {
|
||||
fn handle(&mut self, msg: Result<Message, ProtocolError>, ctx: &mut Self::Context) {
|
||||
let mut pool_conn = match block_on(self.pool.acquire()) {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
log::error!("Failed to acquire database connection: {:?}", err);
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let msg = match msg {
|
||||
Err(_) => {
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
Ok(msg) => msg,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
ctx.pong(&msg)
|
||||
}
|
||||
Message::Pong(_) => {
|
||||
self.hb = Instant::now();
|
||||
}
|
||||
Message::Text(text) => match serde_json::from_str(&text) {
|
||||
Ok(action) => {
|
||||
self.handle_action(&mut pool_conn, ctx, action);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error deserializing action: {:?}", e);
|
||||
ctx.text(
|
||||
serde_json::to_string(&EmgauwaError::Serialization(e))
|
||||
.unwrap_or(String::from("Error in deserializing action")),
|
||||
);
|
||||
}
|
||||
},
|
||||
Message::Binary(_) => log::warn!("Received unexpected binary in controller ws"),
|
||||
Message::Close(reason) => {
|
||||
ctx.close(reason);
|
||||
ctx.stop();
|
||||
}
|
||||
Message::Continuation(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
Message::Nop => (),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||
use actix_web_actors::ws;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::handlers::v1::ws::controllers::ControllersWs;
|
||||
use crate::handlers::v1::ws::relays::RelaysWs;
|
||||
|
||||
pub mod controllers;
|
||||
pub mod relays;
|
||||
|
||||
#[get("/ws/controllers")]
|
||||
pub async fn ws_controllers(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let resp = ws::start(
|
||||
ControllersWs {
|
||||
pool: pool.get_ref().clone(),
|
||||
controller_uid: None,
|
||||
app_state: app_state.get_ref().clone(),
|
||||
hb: Instant::now(),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
)
|
||||
.map_err(|_| EmgauwaError::Internal(String::from("error starting websocket")));
|
||||
resp
|
||||
}
|
||||
|
||||
#[get("/ws/relays")]
|
||||
pub async fn ws_relays(
|
||||
app_state: web::Data<Addr<AppState>>,
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
) -> Result<HttpResponse, EmgauwaError> {
|
||||
let resp = ws::start(
|
||||
RelaysWs {
|
||||
app_state: app_state.get_ref().clone(),
|
||||
hb: Instant::now(),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
)
|
||||
.map_err(|_| EmgauwaError::Internal(String::from("error starting websocket")));
|
||||
resp
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, StreamHandler};
|
||||
use actix_web_actors::ws;
|
||||
use actix_web_actors::ws::ProtocolError;
|
||||
use emgauwa_common::constants::{HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT};
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use futures::executor::block_on;
|
||||
|
||||
use crate::app_state::{AppState, ConnectRelayClient};
|
||||
|
||||
pub struct RelaysWs {
|
||||
pub app_state: Addr<AppState>,
|
||||
pub hb: Instant,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct SendRelays {
|
||||
pub relays_json: String,
|
||||
}
|
||||
|
||||
impl Actor for RelaysWs {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// get unique id for ctx
|
||||
match self.get_relays_json() {
|
||||
Ok(relays_json) => {
|
||||
ctx.text(relays_json);
|
||||
self.hb(ctx);
|
||||
|
||||
block_on(self.app_state.send(ConnectRelayClient {
|
||||
addr: ctx.address(),
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Error getting relays: {:?}", err);
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RelaysWs {
|
||||
fn get_relays_json(&self) -> Result<String, EmgauwaError> {
|
||||
let relays = block_on(self.app_state.send(crate::app_state::GetRelays {}))??;
|
||||
serde_json::to_string(&relays).map_err(EmgauwaError::from)
|
||||
}
|
||||
|
||||
// helper method that sends ping to client every 5 seconds (HEARTBEAT_INTERVAL).
|
||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
// check client heartbeats
|
||||
if Instant::now().duration_since(act.hb) > HEARTBEAT_TIMEOUT {
|
||||
log::debug!("Websocket Relay heartbeat failed, disconnecting!");
|
||||
ctx.stop();
|
||||
// don't try to send a ping
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping(&[]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<ws::Message, ProtocolError>> for RelaysWs {
|
||||
fn handle(&mut self, msg: Result<ws::Message, ProtocolError>, ctx: &mut Self::Context) {
|
||||
let msg = match msg {
|
||||
Err(_) => {
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
Ok(msg) => msg,
|
||||
};
|
||||
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
ctx.pong(&msg)
|
||||
}
|
||||
ws::Message::Pong(_) => {
|
||||
self.hb = Instant::now();
|
||||
}
|
||||
ws::Message::Text(_) => log::debug!("Received unexpected text in relays ws"),
|
||||
ws::Message::Binary(_) => log::debug!("Received unexpected binary in relays ws"),
|
||||
ws::Message::Close(reason) => {
|
||||
ctx.close(reason);
|
||||
ctx.stop();
|
||||
}
|
||||
ws::Message::Continuation(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
ws::Message::Nop => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<SendRelays> for RelaysWs {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: SendRelays, ctx: &mut Self::Context) -> Self::Result {
|
||||
ctx.text(msg.relays_json);
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
use std::net::TcpListener;
|
||||
|
||||
use actix::{Actor, Arbiter};
|
||||
use actix_cors::Cors;
|
||||
use actix_web::middleware::TrailingSlash;
|
||||
use actix_web::{middleware, web, App, HttpServer};
|
||||
use emgauwa_common::db::DbController;
|
||||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::utils::{drop_privileges, init_logging};
|
||||
use serde_json::json;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
mod app_state;
|
||||
mod handlers;
|
||||
mod settings;
|
||||
mod utils;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
let settings = settings::init()?;
|
||||
|
||||
let listener = TcpListener::bind(format!("{}:{}", settings.server.host, settings.server.port))?;
|
||||
drop_privileges(&settings.permissions)?;
|
||||
|
||||
init_logging(&settings.logging.level)?;
|
||||
|
||||
let pool = emgauwa_common::db::init(&settings.database).await?;
|
||||
|
||||
let mut conn = pool.acquire().await.map_err(EmgauwaError::from)?;
|
||||
DbController::all_inactive(&mut conn)
|
||||
.await
|
||||
.map_err(EmgauwaError::from)?;
|
||||
conn.close().await.map_err(EmgauwaError::from)?;
|
||||
|
||||
let app_state_arbiter = Arbiter::with_tokio_rt(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
let app_state_pool = pool.clone();
|
||||
let app_state = Actor::start_in_arbiter(&app_state_arbiter.handle(), move |_| {
|
||||
AppState::new(app_state_pool)
|
||||
});
|
||||
|
||||
log::info!(
|
||||
"Starting server on {}:{}",
|
||||
settings.server.host,
|
||||
settings.server.port
|
||||
);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default().allow_any_method().allow_any_header();
|
||||
|
||||
let origins = settings.origins.clone();
|
||||
let cors = match settings.origins.is_empty() {
|
||||
true => cors.allow_any_origin(),
|
||||
false => cors.allowed_origin_fn(move |origin, _req_head| {
|
||||
origins.contains(&origin.to_str().unwrap_or_default().to_string())
|
||||
}),
|
||||
};
|
||||
|
||||
let api_default = json!({
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"version": "0.0.0",
|
||||
"title": "Failed to load API documentation",
|
||||
}
|
||||
});
|
||||
let api_v1_json =
|
||||
serde_json::from_str(include_str!(concat!(env!("OUT_DIR"), "/api.v1.json")))
|
||||
.unwrap_or(api_default.clone());
|
||||
|
||||
App::new()
|
||||
.wrap(cors)
|
||||
.wrap(middleware::Logger::default())
|
||||
.app_data(web::JsonConfig::default().error_handler(handlers::json_error_handler))
|
||||
.app_data(web::Data::new(pool.clone()))
|
||||
.app_data(web::Data::new(app_state.clone()))
|
||||
.service(
|
||||
SwaggerUi::new("/api/docs/{_:.*}")
|
||||
.external_urls_from_iter_unchecked([("/api/v1.json", api_v1_json)]),
|
||||
)
|
||||
.service(
|
||||
web::scope("/api/v1")
|
||||
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
|
||||
.service(handlers::v1::controllers::index)
|
||||
.service(handlers::v1::controllers::show)
|
||||
.service(handlers::v1::controllers::update)
|
||||
.service(handlers::v1::controllers::delete)
|
||||
.service(handlers::v1::relays::index)
|
||||
.service(handlers::v1::relays::tagged)
|
||||
.service(handlers::v1::relays::index_for_controller)
|
||||
.service(handlers::v1::relays::show_for_controller)
|
||||
.service(handlers::v1::relays::update_for_controller)
|
||||
.service(handlers::v1::relays::pulse)
|
||||
.service(handlers::v1::schedules::index)
|
||||
.service(handlers::v1::schedules::tagged)
|
||||
.service(handlers::v1::schedules::show)
|
||||
.service(handlers::v1::schedules::add)
|
||||
.service(handlers::v1::schedules::add_list)
|
||||
.service(handlers::v1::schedules::update)
|
||||
.service(handlers::v1::schedules::delete)
|
||||
.service(handlers::v1::tags::index)
|
||||
.service(handlers::v1::tags::show)
|
||||
.service(handlers::v1::tags::delete)
|
||||
.service(handlers::v1::tags::add)
|
||||
.service(handlers::v1::macros::index)
|
||||
.service(handlers::v1::macros::show)
|
||||
.service(handlers::v1::macros::add)
|
||||
.service(handlers::v1::macros::update)
|
||||
.service(handlers::v1::macros::delete)
|
||||
.service(handlers::v1::macros::execute)
|
||||
.service(handlers::v1::ws::ws_controllers)
|
||||
.service(handlers::v1::ws::ws_relays),
|
||||
)
|
||||
})
|
||||
.listen(listener)?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
use emgauwa_common::errors::EmgauwaError;
|
||||
use emgauwa_common::settings;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[allow(unused)]
|
||||
pub struct Settings {
|
||||
pub server: settings::Server,
|
||||
pub database: String,
|
||||
pub permissions: settings::Permissions,
|
||||
pub logging: settings::Logging,
|
||||
|
||||
pub origins: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
server: settings::Server::default(),
|
||||
database: String::from("sqlite://emgauwa-core.sqlite"),
|
||||
permissions: settings::Permissions::default(),
|
||||
logging: settings::Logging::default(),
|
||||
|
||||
origins: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() -> Result<Settings, EmgauwaError> {
|
||||
settings::load("core", "CORE")
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
pub fn flatten_result<T, E>(res: Result<Result<T, E>, E>) -> Result<T, E> {
|
||||
match res {
|
||||
Ok(Ok(t)) => Ok(t),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue