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]
 | 
					[package]
 | 
				
			||||||
resolver = "2"
 | 
					name = "emgauwa-common"
 | 
				
			||||||
members = [
 | 
					version = "0.5.0"
 | 
				
			||||||
    "emgauwa-core",
 | 
					edition = "2021"
 | 
				
			||||||
    "emgauwa-controller",
 | 
					authors = ["Tobias Reisinger <tobias@msrg.cc>"]
 | 
				
			||||||
    "emgauwa-common",
 | 
					
 | 
				
			||||||
]
 | 
					
 | 
				
			||||||
 | 
					[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 database create
 | 
				
			||||||
	cargo sqlx migrate run
 | 
						cargo sqlx migrate run
 | 
				
			||||||
	cargo sqlx prepare --workspace
 | 
						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…
	
	Add table
		Add a link
		
	
		Reference in a new issue