├── .editorconfig
├── .env.example
├── .github
└── workflows
│ └── build_and_deploy.yml
├── .gitignore
├── README.md
├── build.sh
├── docker-compose-frontend.yml
├── docker-compose-portainer.yml
├── docker-compose-rtl433.yaml
├── docker-compose-server.yml
├── docker-compose-zigbee2mqtt.yml
├── docker-compose.yml
├── docs
├── DOCKER.md
├── FRONTEND.md
├── README.md
├── SERVER.md
└── screenshots
│ ├── listing.png
│ └── raspiscan.jpg
├── frontend
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .stylelintrc
├── Dockerfile
├── config
│ ├── BrowserSync.js
│ ├── WebpackConfigClass.js
│ ├── WebpackConfigCommon.js
│ ├── WebpackConfigDev.js
│ ├── WebpackConfigProd.js
│ └── WebpackRun.js
├── dist
│ └── prod
│ │ ├── app.js
│ │ ├── index.html
│ │ └── version
├── package-lock.json
├── package.json
├── public
│ ├── css
│ │ └── fonts
│ │ │ └── Barlow
│ │ │ ├── barlow-v12-latin-100.woff2
│ │ │ ├── barlow-v12-latin-200.woff2
│ │ │ ├── barlow-v12-latin-300.woff2
│ │ │ ├── barlow-v12-latin-500.woff2
│ │ │ ├── barlow-v12-latin-600.woff2
│ │ │ ├── barlow-v12-latin-700.woff2
│ │ │ ├── barlow-v12-latin-800.woff2
│ │ │ ├── barlow-v12-latin-900.woff2
│ │ │ └── barlow-v12-latin-regular.woff2
│ ├── dev.html
│ ├── favicon.ico
│ ├── index.html
│ └── index_template.html
└── src
│ ├── app.js
│ ├── lib
│ ├── Global
│ │ ├── Events.js
│ │ ├── Globals.js
│ │ ├── Log.js
│ │ ├── Module.js
│ │ ├── Ramda.js
│ │ └── Utils.js
│ ├── Home
│ │ ├── Templates
│ │ │ ├── Device.html
│ │ │ ├── DeviceTopics.html
│ │ │ ├── Exclude.html
│ │ │ ├── Topic.html
│ │ │ ├── TopicAdd.html
│ │ │ └── layout.html
│ │ ├── device.js
│ │ ├── devices.js
│ │ ├── excludes.js
│ │ ├── index.js
│ │ ├── topic.js
│ │ └── topics.js
│ ├── Icons
│ │ ├── book.html
│ │ ├── check.html
│ │ ├── close.html
│ │ ├── eye.html
│ │ ├── eye_alt.html
│ │ ├── heart.html
│ │ ├── home.html
│ │ ├── index.js
│ │ ├── mouth.html
│ │ ├── music.html
│ │ ├── options.html
│ │ ├── pause.html
│ │ ├── pen.html
│ │ ├── play.html
│ │ ├── plus.html
│ │ ├── podcast.html
│ │ ├── skip-next.html
│ │ ├── skip-prev.html
│ │ ├── stop.html
│ │ ├── svg
│ │ │ ├── book.svg
│ │ │ ├── check.svg
│ │ │ ├── close.svg
│ │ │ ├── eye.svg
│ │ │ ├── eye_alt.svg
│ │ │ ├── heart.svg
│ │ │ ├── home.svg
│ │ │ ├── mouth.svg
│ │ │ ├── music.svg
│ │ │ ├── options.svg
│ │ │ ├── pause.svg
│ │ │ ├── pen.svg
│ │ │ ├── play.svg
│ │ │ ├── plus.svg
│ │ │ ├── podcast.svg
│ │ │ ├── skip-next.svg
│ │ │ ├── skip-prev.svg
│ │ │ ├── stop.svg
│ │ │ └── user.svg
│ │ └── user.html
│ ├── Locale
│ │ ├── Translations.js
│ │ ├── de.json
│ │ ├── en.json
│ │ └── index.js
│ ├── Main.js
│ ├── Navigation
│ │ ├── Templates
│ │ │ └── navigation.html
│ │ └── index.js
│ └── Tab.js
│ └── scss
│ ├── app.scss
│ ├── global
│ ├── _index.scss
│ ├── background.scss
│ ├── base.scss
│ ├── button.scss
│ ├── colors.scss
│ ├── form.scss
│ ├── modal.scss
│ ├── responsive.scss
│ ├── tab.scss
│ └── typo.scss
│ └── modules
│ ├── _index.scss
│ ├── devices
│ └── _index.scss
│ ├── excludes
│ └── _index.scss
│ ├── home
│ └── _index.scss
│ ├── navigation
│ └── _index.scss
│ └── topics
│ ├── _index.scss
│ ├── add.scss
│ └── topics.scss
├── rtl_433
├── rtl-sdr.rules
└── rtl_433.conf
├── server
├── .babelrc
├── .dockerignore
├── Dockerfile
├── app.js
├── config
│ ├── default.conf.example
│ ├── excludes.json.example
│ ├── mapping.json.example
│ └── types.json
├── entrypoint.sh
├── index.js
├── lib
│ ├── Globals.js
│ ├── Mqtt
│ │ ├── Client.js
│ │ └── index.js
│ ├── RTL433
│ │ ├── Device.js
│ │ ├── DeviceTopic.js
│ │ ├── Excludes.js
│ │ ├── Topics.js
│ │ └── index.js
│ └── Server
│ │ ├── Route.js
│ │ ├── base64.js
│ │ ├── index.js
│ │ └── routes
│ │ ├── Device.js
│ │ ├── Devices.js
│ │ ├── Exclude.js
│ │ ├── Excludes.js
│ │ ├── Home.js
│ │ ├── Topic.js
│ │ ├── Topics.js
│ │ └── index.js
├── package.json
├── testing.js
└── webpack-app-pkg.config.js
├── setup.sh
├── setupBuildx.sh
├── shared
├── lib
│ ├── Config.js
│ ├── Events.js
│ ├── Log.js
│ ├── Module.js
│ └── Utils.js
└── package.json
└── zigbee2mqtt
└── configuration.yaml.example
/.env.example:
--------------------------------------------------------------------------------
1 | # globals
2 | # ---------------------------------------------------
3 |
4 | # used for naming
5 | PROJECT_NAME=raspiscan
6 |
7 | # the docker compose binary from github
8 | DOCKER_COMPOSE_SOURCE=https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-linux-armv7
9 |
10 | # docker build
11 | DOCKER_IMAGE_VERSION=0.2
12 | DOCKER_IMAGE_NAME=node-rtl433-ui
13 | DOCKER_REGISTRY=ghcr.io/seekwhencer
14 | DOCKER_BUILDX_PLATFORM=linux/arm64/v8,linux/amd64
15 | DOCKER_BUILDX_NAME=spachtelmasse
16 | DOCKER_QEMU_VERSION=7.2.0-1
17 |
18 | # the target for pkg binary (obsolete)
19 | BUILD_FILENAME=server_arm64
20 | BUILD_TARGET=node16-linux-arm64
21 |
22 | # Server App
23 | # ---------------------------------------------------
24 |
25 | # as title ;)
26 | SERVER_PORT=3000
27 |
28 | # container outside
29 | SERVER_PORT_OUTSIDE=3000
30 |
31 | # the RELATIVE path to the frontend production bundle
32 | SERVER_FRONTEND_PATH=../frontend/dist/prod
33 |
34 | # console output on topic event
35 | SERVER_LOG_TOPICS=true
36 |
37 | # USB Device
38 | # ---------------------------------------------------
39 | USB_DEVICE_SOURCE=/dev/bus/usb/001/003
40 | USB_DEVICE_TARGET=/dev/bus/usb/001/003
41 |
42 | # MQTT
43 | # ---------------------------------------------------
44 | # this is the reachabe ip of the mqtt broker - used by extra hosts in compose file
45 | MQTT_HOST_IP=192....
46 | # docker network internal name, set by extra_hosts in compose file
47 | MQTT_HOST=broker
48 | # the mqtt port of broker
49 | MQTT_PORT=1886
50 |
51 | # Frontend Dev
52 | # ---------------------------------------------------
53 | # the dev proxy port
54 | FRONTEND_SERVER_PORT=4000
55 |
56 | # the hostname (docker hostname = service name)
57 | FRONTEND_PROXY_TARGET_HOST=raspiscan_server
58 |
59 | # the server port. same as SERVER_PORT
60 | FRONTEND_PROXY_TARGET_PORT=3000
61 |
--------------------------------------------------------------------------------
/.github/workflows/build_and_deploy.yml:
--------------------------------------------------------------------------------
1 | name: build_and_deploy
2 | on:
3 | push:
4 | branches:
5 | - master
6 | paths:
7 | - frontend/**
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 | defaults:
12 | run:
13 | working-directory: frontend
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3
17 |
18 | - name: Setup Node
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: '16'
22 |
23 | - name: Run frontend build
24 | run: |
25 | npm install --legacy-peer-deps
26 | node --experimental-modules --experimental-json-modules config/WebpackConfigProd.js
27 |
28 | - name: Deploy
29 | uses: JamesIves/github-pages-deploy-action@releases/v4
30 | with:
31 | branch: frontend-production
32 | folder: frontend/dist/prod
33 | clean: true
34 | single-commit: true
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/**/*
2 | node_modules
3 | .hot
4 | npm-debug.log
5 | .env
6 |
7 | frontend/dist
8 | server/dist
9 | server/config/**/*
10 |
11 | rtl_433/mapping.json
12 | zigbee2mqtt/**/*
13 |
14 | !.gitkeep
15 | !server/config/*.example
16 | !server/config/type.json
17 | !zigbee2mqtt/configuration.yaml.example
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # node-rtl433-ui
2 |
3 | This is a simple and user guided, graphical way to map a value from a 433 Mhz device to a MQTT topic.
4 |
5 | - scan for 433Mhz devices with [merbanan/rtl_433](https://github.com/merbanan/rtl_433)
6 | - map a **device** and **field** to a **topic** per web ui.
7 | - server works as MQTT client to your broker
8 | - server delivers the frontend statics and some api endpoints
9 | - server sends a value (single float value, not json) from a device on a specific MQTT topic to your broker - only when the value changes
10 | - server can update mapping at runtime
11 | - ui language can be edited and set
12 | - drop topic (delete mapped topic for a device and value)
13 | - add model to exclude list
14 | - add device to exclude list
15 | - drop entry from exclude list
16 | - sort device listing by *last update* or *signal count*
17 | - enable and disable list update
18 | - forget unmapped device after x seconds / minutes
19 | - enable and disable removing unmapped devices from list
20 |
21 | 
22 |
23 | ## Setup (Raspberry Pi 4)
24 | - take the lite OS image without desktop
25 | - set up your raspberry pi as you will (expand filesystem, enable ssh, disable bluetooth and wifi etc.)
26 | - install docker and docker compose, create docker volumes
27 | ```bash
28 | # use your home folder
29 | cd ~
30 |
31 | # clone repo
32 | git clone https://github.com/seekwhencer/node-rtl433-ui.git raspiscan
33 |
34 | # the folder
35 | cd raspiscan # or: /home/pi/raspiscan or: ~/raspiscan
36 |
37 | # make the setup script runable
38 | chmod +x ./setup.sh
39 |
40 | # run the script not as sudo
41 | ./setup.sh
42 | ```
43 |
44 | ## Config
45 |
46 | duplicate *.example files as *
47 |
48 | - edit the file `.env`
49 | - edit the file `server/config/default.conf`
50 | - edit the file `rtl433/rtl_433.conf` (if needed)
51 |
52 | ## Run
53 |
54 | - ```bash
55 | docker-compose up -d
56 | ```
57 | > Now open: http://RASPBERRYPI:3000
58 |
59 | ## Roadmap
60 | what's next?
61 | - ui get the forget state at start
62 |
63 | ## In the wild
64 | 
65 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #
4 | # run this script on a docker host with buildx
5 | #
6 |
7 | # load .env file
8 | loadConfig() {
9 | DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10 | export $(egrep -v '^#' "${DIR}/.env" | xargs)
11 |
12 | export TAG_LATEST="${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:latest"
13 | export TAG_VERSION="${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_VERSION}"
14 |
15 | }
16 |
17 | # build the docker image
18 | build() {
19 | env
20 | #docker build . -t ${TAG_LATEST} -t ${TAG_VERSION} -f ./server/Dockerfile
21 |
22 | docker buildx build . --no-cache --builder=${DOCKER_BUILDX_NAME} --platform=${DOCKER_BUILDX_PLATFORM} --push -t ${TAG_LATEST} -t ${TAG_VERSION} -f ./server/Dockerfile
23 | }
24 |
25 | #
26 | loadConfig
27 | build
--------------------------------------------------------------------------------
/docker-compose-frontend.yml:
--------------------------------------------------------------------------------
1 | version: "3.6"
2 |
3 | networks:
4 |
5 | raspiscan:
6 | external: false
7 | name: ${PROJECT_NAME}
8 |
9 | services:
10 |
11 | kidsplayer_frontend:
12 | build:
13 | context: .
14 | dockerfile: frontend/Dockerfile
15 | image: ${PROJECT_NAME}_frontend
16 | working_dir: /raspiscan/frontend
17 | command: 'tail -f /dev/null'
18 |
19 | # or run on container start
20 | # command: "--experimental-modules --experimental-json-modules config/WebpackConfigDev.js"
21 |
22 | container_name: ${PROJECT_NAME}_frontend
23 | volumes:
24 | - .:/raspiscan
25 | networks:
26 | - raspiscan
27 | ports:
28 | - '${FRONTEND_SERVER_PORT}:${FRONTEND_SERVER_PORT_OUTSIDE}'
29 | environment:
30 | - DEBUG=true
31 | - ENVIRONMENT=default
32 | - PROXY_TARGET_HOST=${FRONTEND_PROXY_TARGET_HOST}
33 | - PROXY_TARGET_PORT=${FRONTEND_PROXY_TARGET_PORT}
34 | - SERVER_PORT=${FRONTEND_SERVER_PORT}
35 |
--------------------------------------------------------------------------------
/docker-compose-portainer.yml:
--------------------------------------------------------------------------------
1 | version: '3.1'
2 |
3 | volumes:
4 | portainer_data:
5 | driver: local
6 |
7 | services:
8 | portainer:
9 | image: portainer/portainer
10 | container_name: "portainer-app"
11 | restart: always
12 | command: -H unix:///var/run/docker.sock
13 | ports:
14 | - "9000:9000"
15 | volumes:
16 | - /var/run/docker.sock:/var/run/docker.sock
17 | - /etc/localtime:/etc/localtime:ro
18 | - /etc/timezone:/etc/timezone:ro
19 | - portainer_data:/data
20 |
--------------------------------------------------------------------------------
/docker-compose-rtl433.yaml:
--------------------------------------------------------------------------------
1 | version: "3.6"
2 |
3 | services:
4 |
5 | rtl433:
6 | restart: always
7 | image: hertzg/rtl_433:latest
8 | container_name: ${PROJECT_NAME}-rtl433
9 | command: "-F http"
10 | devices:
11 | - /dev/bus/usb/001/003:/dev/bus/usb/001/003
12 | ports:
13 | - "8433:8433"
14 | volumes:
15 | - ./rtl_433/rtl_433.conf:/etc/rtl_433/rtl_433.conf
16 |
17 |
--------------------------------------------------------------------------------
/docker-compose-server.yml:
--------------------------------------------------------------------------------
1 | version: "3.6"
2 |
3 | networks:
4 |
5 | raspiscan:
6 | external: false
7 | name: ${PROJECT_NAME}
8 |
9 | services:
10 |
11 | raspiscan_server:
12 | build:
13 | context: .
14 | dockerfile: server/Dockerfile
15 | image: ${PROJECT_NAME}_server
16 | working_dir: /raspiscan/server
17 |
18 | # the app is not starting !
19 | # just enter:
20 | #
21 | # docker exec -it raspiscan_server /bin/sh -c "node --experimental-modules --experimental-json-modules index.js"
22 | #
23 | # to run the server
24 |
25 | command: 'tail -f /dev/null'
26 |
27 | user: root
28 | container_name: ${PROJECT_NAME}_server
29 | restart: always
30 | volumes:
31 | - .:/raspiscan
32 | - ./rtl_433/rtl_433.conf:/etc/rtl_433/rtl_433.conf
33 | - ./rtl_433/rtl-sdr.rules:/etc/udev/rules.d/rtl-sdr.rules
34 | networks:
35 | - raspiscan
36 | ports:
37 | - "${SERVER_PORT}:${SERVER_PORT_OUTSIDE}"
38 | extra_hosts:
39 | - "${MQTT_HOST}:${MQTT_HOST_IP}"
40 | environment:
41 | - DEBUG=true
42 | - VERBOSE=2
43 | - ENVIRONMENT=default
44 | - NODE_ENV=development
45 | - PWD=/raspiscan/server
46 | # important change for development
47 | - SERVER_FRONTEND_PATH=../frontend/dist/dev
48 | devices:
49 | - "${USB_DEVICE_SOURCE}:${USB_DEVICE_TARGET}"
50 | privileged: true
51 |
52 |
--------------------------------------------------------------------------------
/docker-compose-zigbee2mqtt.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | networks:
4 |
5 | raspiscan:
6 | external: false
7 | name: ${PROJECT_NAME}
8 |
9 | services:
10 |
11 | zigbee2mqtt:
12 | container_name: ${PROJECT_NAME}_zigbee2mqtt
13 | restart: always
14 | #command: 'tail -f /dev/null'
15 | privileged: false
16 | image: koenkk/zigbee2mqtt:latest
17 | volumes:
18 | - ./zigbee2mqtt:/app/data
19 | - /run/udev:/run/udev:ro
20 | networks:
21 | - raspiscan
22 | ports:
23 | - "${ZIGBEE_PORT}:${ZIGBEE_PORT_OUTSIDE}/tcp"
24 | extra_hosts:
25 | - "${MQTT_HOST}:${MQTT_HOST_IP}"
26 | environment:
27 | - TZ=Europe/Berlin
28 | devices:
29 | - /dev/serial/by-id/usb-Silicon_Labs_Sonoff_Zigbee_3.0_USB_Dongle_Plus_0001-if00-port0:/dev/ttyUSB0
30 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.6"
2 |
3 | networks:
4 |
5 | raspiscan:
6 | external: false
7 | name: ${PROJECT_NAME}
8 |
9 | services:
10 |
11 | raspiscan_server:
12 | image: ghcr.io/seekwhencer/node-rtl433-ui:${DOCKER_IMAGE_VERSION}
13 | working_dir: /${PROJECT_NAME}/server
14 | command: '--experimental-modules --experimental-json-modules index.js'
15 | user: root
16 | container_name: ${PROJECT_NAME}_server
17 | restart: always
18 | volumes:
19 | - .env:/raspiscan/.env
20 | - ./server/config:/raspiscan/server/config
21 |
22 | # or only the config file
23 | # - ./server/config/default.conf:/raspiscan/server/config/default.conf
24 | networks:
25 | - raspiscan
26 | ports:
27 | - "${SERVER_PORT}:${SERVER_PORT_OUTSIDE}"
28 | extra_hosts:
29 | - "${MQTT_HOST}:${MQTT_HOST_IP}"
30 | environment:
31 | - DEBUG=true
32 | - VERBOSE=2
33 | # equals the config file: server/config/{ENVIRONMENT}.conf
34 | - ENVIRONMENT=default
35 | - NODE_ENV=production
36 | - PWD=/raspiscan/server
37 | # important, don't change it. the frontend production bundle is placed there
38 | - SERVER_FRONTEND_PATH=frontend
39 | # log topic and device events
40 | - SERVER_LOG_TOPICS=false
41 | devices:
42 | - "${USB_DEVICE_SOURCE}:${USB_DEVICE_TARGET}"
43 | privileged: true
44 |
45 |
--------------------------------------------------------------------------------
/docs/DOCKER.md:
--------------------------------------------------------------------------------
1 | ## *node-rtl433-ui*
2 | # Docker
3 |
4 | You can drop all files from this repo, except:
5 |
6 | ```bash
7 | .env
8 | docker-compose.yml
9 | server/config/default.conf
10 | ```
11 |
12 |
13 | ## Todo
--------------------------------------------------------------------------------
/docs/FRONTEND.md:
--------------------------------------------------------------------------------
1 | ## *node-rtl433-ui*
2 | # Frontend
3 |
4 | - The frontend production bundle is not part of this repository.
5 | - A github action builds the production bundle and push it to the branch: [frontend-production](https://github.com/seekwhencer/node-rtl433-ui/tree/frontend-production)
6 | - The `server/entrypoint.sh` places the content of the **frontend-production** branch into: `server/config/frontend`
7 | - For production:`SERVER_FRONTEND_PATH=config/frontend` set in `docker-compose.yml`
8 | - For development:`SERVER_FRONTEND_PATH=../frontend/dist/dev` set in `docker-compose-server.yml`
9 |
10 | ## Development
11 | - ### frontend dev build pipeline with file watcher and proxy
12 | > *on a second console*
13 | ```bash
14 | # start only the container
15 | docker-compose -f docker-compose-frontend.yml
16 |
17 | # start the file watcher build pipeline
18 | docker exec -it raspiscan_frontend /bin/sh -c "node --experimental-modules --experimental-json-modules config/WebpackConfigDev.js"
19 | ```
20 |
21 | > Now open: http://RASPBERRYPI:4000/dev.html
22 |
23 |
24 | - ### frontend production build
25 | ```bash
26 | # start only the container - if not running
27 | docker-compose -f docker-compose-frontend.yml
28 |
29 | # bundling
30 | docker exec -it raspiscan_frontend /bin/sh -c "node --experimental-modules --experimental-json-modules config/WebpackConfigProd.js"
31 | ```
32 | Creates a bundle in `frontend/dist/prod`
33 |
34 | > This will be triggered by a github workflow action and only the result will be pushed to the branch: [frontend-production](https://github.com/seekwhencer/node-rtl433-ui/tree/frontend-production)
35 |
36 | ## Todo
37 | - ...
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # node-rtl433-ui
2 |
3 |
--------------------------------------------------------------------------------
/docs/SERVER.md:
--------------------------------------------------------------------------------
1 | ## *node-rtl433-ui*
2 | # Server
3 |
4 | ## Production
5 | - ```bash
6 | docker-compose up -d
7 | ```
8 | > Now open: http://RASPBERRYPI:3000
9 |
10 | ## Development
11 |
12 | - ### stop production container
13 | ```bash
14 | docker-compose down
15 | ```
16 | - ### start server container in dev mode
17 | ```bash
18 | # start server container in dev mode, not the server
19 | docker-compose -f docker-compose-server.yml up -d
20 |
21 | # start the server
22 | docker exec -it raspiscan_server /bin/sh -c "node --experimental-modules --experimental-json-modules index.js"
23 | ```
24 |
25 | ## Todo
--------------------------------------------------------------------------------
/docs/screenshots/listing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/docs/screenshots/listing.png
--------------------------------------------------------------------------------
/docs/screenshots/raspiscan.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/docs/screenshots/raspiscan.jpg
--------------------------------------------------------------------------------
/frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 4
8 | indent_style = space
9 | insert_final_newline = true
10 | max_line_length = 80
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | max_line_length = 0
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": "eslint:recommended",
7 | "parserOptions": {
8 | "ecmaVersion": 9,
9 | "sourceType": "module"
10 | },
11 | "rules": {
12 | "no-redeclare": "off",
13 | "no-undef": "off",
14 | "no-unused-vars": "off",
15 | "no-fallthrough" : "off",
16 | "no-constant-condition" : "off",
17 | "no-duplicate-case" : "off",
18 | "no-useless-escape" : "off",
19 | "no-case-declarations": "off"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .hot
3 | node_modules
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/frontend/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard-scss",
3 | "rules": {
4 | "max-line-length": null,
5 | "block-no-empty": null,
6 | "color-no-invalid-hex": true,
7 | "comment-empty-line-before": [
8 | "always",
9 | {
10 | "ignore": [
11 | "stylelint-commands",
12 | "after-comment"
13 | ]
14 | }
15 | ],
16 | "declaration-colon-space-after": "always",
17 | "indentation": [
18 | 4,
19 | {
20 | "except": [
21 | "value"
22 | ]
23 | }
24 | ],
25 | "max-empty-lines": 2,
26 | "rule-empty-line-before": [
27 | "always",
28 | {
29 | "except": [
30 | "first-nested"
31 | ],
32 | "ignore": [
33 | "after-comment"
34 | ]
35 | }
36 | ],
37 | "unit-allowed-list": [
38 | "px",
39 | "em",
40 | "rem",
41 | "%",
42 | "s",
43 | "deg",
44 | "fr",
45 | "vw",
46 | "vh"
47 | ],
48 | "at-rule-allowed-list": [
49 | "media",
50 | "mixin",
51 | "content",
52 | "font-face",
53 | "include",
54 | "keyframes",
55 | "import"
56 | ],
57 | "scss/at-import-no-partial-leading-underscore": null,
58 | "scss/at-import-partial-extension": "always",
59 | "selector-pseudo-element-colon-notation": "single",
60 | "no-descending-specificity": null,
61 | "color-function-notation": null,
62 | "alpha-value-notation": "number",
63 | "selector-class-pattern": null,
64 | "keyframes-name-pattern": null,
65 | "declaration-block-no-redundant-longhand-properties": null,
66 | "font-family-no-missing-generic-family-keyword": null,
67 | "font-family-name-quotes": null,
68 | "scss/no-global-function-names": null,
69 | "selector-attribute-quotes": "never",
70 | "scss/dollar-variable-empty-line-before": "never"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16-alpine
2 |
3 | VOLUME ["/raspiscan/frontend/node_modules"]
4 |
5 | RUN npm config set legacy-peer-deps true
6 |
7 | WORKDIR /raspiscan/frontend
8 | COPY frontend/package.json .
9 | RUN npm install
10 | COPY frontend .
11 |
12 | WORKDIR /raspiscan
13 | COPY .env.example .env
14 |
--------------------------------------------------------------------------------
/frontend/config/BrowserSync.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import browserSync from "browser-sync";
3 | import {createProxyMiddleware} from "http-proxy-middleware";
4 | import {responseInterceptor} from "http-proxy-middleware";
5 |
6 | export default class {
7 | constructor(parent) {
8 | this.parent = parent;
9 | this.proxyMiddleware = createProxyMiddleware;
10 | this.responseInterceptor = responseInterceptor;
11 |
12 | this.port = parseInt(SERVER_PORT) || 4000;
13 | this.proxyTargetHost = PROXY_TARGET_HOST || 'localhost';
14 | this.proxyTargetPort = PROXY_TARGET_PORT || 3000;
15 | this.proxyTarget = `http://${this.proxyTargetHost}${this.proxyTargetPort === 80 ? null : `:${this.proxyTargetPort}`}`;
16 | this.bundePath = path.resolve(`${process.cwd()}/dist/dev`);
17 |
18 | console.log('');
19 | console.log('>>>', this.proxyTarget);
20 | console.log('>>> BUNDLE PATH:', this.bundePath);
21 | console.log('');
22 |
23 | this.engine = browserSync.create();
24 |
25 | this.proxy = this.proxyMiddleware({
26 | target: this.proxyTarget,
27 | changeOrigin: true,
28 | secure: false,
29 | rejectUnauthorized: false
30 | });
31 |
32 | this.engine.init({
33 | watch: true,
34 | cwd: `${this.bundePath}/css/`,
35 | injectChanges: true,
36 | files: ["*.css"],
37 | port: this.port,
38 | open: false,
39 | reloadDebounce: 50,
40 | server: {
41 | middleware: [this.proxy]
42 | }
43 | });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/config/WebpackConfigClass.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import WebpackRun from './WebpackRun.js';
3 | import WebpackConfigCommon from './WebpackConfigCommon.js';
4 | import { merge } from 'webpack-merge';
5 |
6 | export default class WebpackConfigClass {
7 | constructor() {
8 | this.getEnv();
9 | this.appPath = `${path.resolve(process.env.PWD)}`;
10 | this.common = new WebpackConfigCommon(this);
11 | this.runner = new WebpackRun(this);
12 | }
13 |
14 | merge() {
15 | this.config = merge(this.common.config, this.config);
16 | }
17 |
18 | run(){
19 | this.runner.run();
20 | }
21 |
22 | getEnv() {
23 | Object.keys(process.env).forEach(i => {
24 | console.log('>>> ENV:', i, process.env[i])
25 | global[i.toUpperCase()] = process.env[i];
26 | });
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/config/WebpackConfigCommon.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import CopyWebpackPlugin from 'copy-webpack-plugin';
3 |
4 | export default class WebpackConfigCommon {
5 | constructor(parent) {
6 | this.parent = parent;
7 | this.build();
8 | }
9 |
10 | build() {
11 | this.config = {
12 |
13 | plugins: [
14 | new CopyWebpackPlugin({
15 | patterns: [
16 | {
17 | from: path.resolve(this.parent.appPath, './public'),
18 | to: '.'
19 | }
20 | ]
21 | })
22 | ],
23 |
24 | resolve: {
25 | alias: {
26 | '~': path.resolve(this.parent.appPath, './src'),
27 | },
28 | },
29 |
30 | module: {
31 | rules: [
32 | {
33 | test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg)(\?.*)?$/,
34 | use: {
35 | loader: 'file-loader',
36 | options: {
37 | name: '[path][name].[ext]',
38 | },
39 | },
40 | },
41 | {
42 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
43 | use: [
44 | {
45 | loader: 'file-loader',
46 | options: {
47 | name: '[name].[ext]',
48 | outputPath: './prod/css/fonts/'
49 | }
50 | }
51 | ]
52 | }
53 | ],
54 | },
55 | };
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/frontend/config/WebpackConfigDev.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import WebpackConfigClass from './WebpackConfigClass.js';
3 | import StyleLintPlugin from "stylelint-webpack-plugin";
4 | import ESLintPlugin from "eslint-webpack-plugin";
5 |
6 |
7 | class WebpackDev extends WebpackConfigClass {
8 | constructor(options) {
9 | super();
10 |
11 | this.options = options | {silent: false}
12 | this.proxyTargetHost = PROXY_TARGET_HOST || 'localhost';
13 | this.proxyTargetPort = PROXY_TARGET_PORT || '3000';
14 |
15 | this.build();
16 | this.merge();
17 |
18 | !this.options.silent ? this.run() : false;
19 | }
20 |
21 | build() {
22 | this.config = {
23 | entry: {
24 | app: ['./src/app.js', './src/scss/app.scss']
25 | },
26 |
27 | target: 'web',
28 | mode: 'development',
29 |
30 | devtool: 'eval-source-map',
31 |
32 | output: {
33 | path: `${this.appPath}/dist/dev`,
34 | filename: './js/[name].js',
35 | hotUpdateChunkFilename: `../.hot/hot-update.js`,
36 | hotUpdateMainFilename: `../.hot/hot-update.json`
37 | },
38 |
39 | optimization: {
40 | removeAvailableModules: false,
41 | removeEmptyChunks: false,
42 | splitChunks: false,
43 | },
44 |
45 | plugins: [
46 | // js
47 | new ESLintPlugin({
48 | extensions: 'js',
49 | emitWarning: true,
50 | files: path.resolve(this.appPath, './src'),
51 | }),
52 |
53 | // scss
54 | new StyleLintPlugin({
55 | configFile: path.resolve(this.appPath, './.stylelintrc'),
56 | files: path.join('src', '**/*.s?(a|c)ss'),
57 | }),
58 |
59 | ],
60 | module: {
61 | rules: [
62 | {
63 | test: /\.html?$/,
64 | loader: "template-literals-loader"
65 | },
66 | {
67 | test: /\.scss$/, use: ['style-loader', {
68 | loader: 'file-loader', options: {
69 | name: '[name].css',
70 | outputPath: '../../dist/dev/css/'
71 | }
72 | }, {
73 | loader: 'sass-loader', options: {
74 | sourceMap: true,
75 | },
76 | }],
77 | }],
78 | },
79 |
80 | watch: true,
81 |
82 | watchOptions: {
83 | aggregateTimeout: 300,
84 | poll: 300,
85 | ignored: ['**/node_modules'],
86 | }
87 | };
88 | }
89 | }
90 |
91 | // run it
92 | new WebpackDev();
93 |
94 |
--------------------------------------------------------------------------------
/frontend/config/WebpackConfigProd.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import WebpackConfigClass from './WebpackConfigClass.js';
3 | import StyleLintPlugin from "stylelint-webpack-plugin";
4 | import ESLintPlugin from "eslint-webpack-plugin";
5 | import TerserPlugin from 'terser-webpack-plugin';
6 | import CopyWebpackPlugin from "copy-webpack-plugin";
7 | import {CleanWebpackPlugin} from "clean-webpack-plugin";
8 |
9 | class WebpackProd extends WebpackConfigClass {
10 | constructor(options) {
11 | super();
12 |
13 | this.options = options | {silent: false}
14 |
15 | this.build();
16 | this.merge();
17 | !this.options.silent ? this.run() : false;
18 | }
19 |
20 | build() {
21 | this.config = {
22 | entry: {
23 | app: ['./src/app.js', './src/scss/app.scss']
24 | },
25 |
26 | target: 'web',
27 | mode: 'production',
28 |
29 | output: {
30 | path: `${this.appPath}/dist/prod`,
31 | filename: './js/[name].js'
32 | },
33 |
34 | plugins: [
35 | new ESLintPlugin({
36 | extensions: 'js',
37 | emitWarning: true,
38 | files: path.resolve(this.appPath, './src'),
39 | }),
40 |
41 | // scss
42 | new StyleLintPlugin({
43 | configFile: path.resolve(this.appPath, './.stylelintrc'),
44 | files: path.join('src', '**/*.s?(a|c)ss'),
45 | }),
46 |
47 | //
48 | new CleanWebpackPlugin(),
49 | //
50 | new CopyWebpackPlugin({
51 | patterns: [
52 | {
53 | from: path.resolve(this.appPath, './public'),
54 | to: '.',
55 | globOptions: {
56 | ignore: ["**/dev.*"],
57 | }
58 | }
59 | ]
60 | })
61 | ],
62 |
63 | optimization: {
64 | minimize: true, minimizer: [new TerserPlugin({
65 | parallel: true,
66 | terserOptions: {
67 | mangle: true,
68 | compress: true, // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
69 | }
70 | }),],
71 | },
72 |
73 | module: {
74 | rules: [
75 | {
76 | test: /\.html?$/, loader: "template-literals-loader"
77 | },
78 | {
79 | test: /\.scss$/, use: ['style-loader', {
80 | loader: 'file-loader', options: {
81 | name: '[name].css',
82 | outputPath: '../../dist/prod/css/'
83 | }
84 | }, {
85 | loader: 'sass-loader', options: {
86 | sourceMap: false,
87 | },
88 | }],
89 | }]
90 | }
91 | };
92 | }
93 | }
94 |
95 | // run it
96 | new WebpackProd();
97 |
98 |
--------------------------------------------------------------------------------
/frontend/config/WebpackRun.js:
--------------------------------------------------------------------------------
1 | import webpack from "webpack";
2 | import BrowserSync from "./BrowserSync.js";
3 | import fs from "fs-extra";
4 |
5 | /**
6 | * i found no way to embed the js bundle
7 | * i tried it as base64, as base64 and uri encoded / decoded
8 | * no way. so it results a base64 css bundle with base64 embedded fonts
9 | * as pack.html
10 | */
11 |
12 | export default class WebpackRun {
13 | constructor(parent) {
14 | this.parent = parent;
15 | this.bundler = false;
16 | this.server = false;
17 | this.package = {};
18 | this.hash = false;
19 | this.browserSync = false;
20 |
21 | this.docRoot = `${this.parent.appPath}/dist/prod`;
22 | this.cssRoot = `${this.docRoot}/css`;
23 | this.jsRoot = `${this.docRoot}/js`;
24 |
25 | this.faviconFileName = `${this.docRoot}/favicon.ico`;
26 | this.htmlFileNameFrom = `${this.docRoot}/index_template.html`;
27 | this.htmlFileNameTo = `${this.docRoot}/index.html`;
28 | this.cssFileName = `${this.cssRoot}/app.css`;
29 | this.jsFileName = `${this.jsRoot}/app.js`;
30 | }
31 |
32 | run() {
33 | this.config = this.parent.config;
34 | this.parent.config.mode === 'development' ? this.runDev() : this.runProd();
35 | }
36 |
37 | runDev() {
38 | this.browserSync = new BrowserSync(this);
39 |
40 | this.bundler = webpack(this.config);
41 | this.watching = this.bundler.watch(this.config.watchOptions, (err, stats) => {
42 | if (err || stats.hasErrors()) {
43 | err ? console.log('>>> ERROR: ', err.message) : null;
44 | stats.hasErrors() ? console.log('>>> ERROR: ', stats.compilation.errors) : null;
45 | } else {
46 | console.log('>>> BUNDLING COMPLETE');
47 | }
48 | });
49 | }
50 |
51 | runProd() {
52 | console.log('');
53 | this.bundler = webpack(this.config, async (err, stats) => {
54 | if (err || stats.hasErrors()) {
55 | console.log('>>> ERROR: ', err.message);
56 | } else {
57 | this.hash = stats.compilation.hash;
58 | console.log('>>> BUNDLING COMPLETE', this.hash);
59 |
60 | //await this.rewriteHTML(/hash/g, parseInt(new Date().getTime() / 1000));
61 |
62 | // do the base64 packaging
63 | await this.pack();
64 | console.log('>>> PACK COMPLETE', this.htmlFileNameTo);
65 |
66 | await this.writeHash();
67 | await this.clean();
68 | }
69 | });
70 | }
71 |
72 | // (unused) old style for linked prod index.html
73 | async rewriteHTML(from, to) {
74 | console.log('>>> REWRITE HTML');
75 | const indexHTMLFile = `${this.parent.appPath}/dist/prod/index.html`;
76 | return fs.readFile(indexHTMLFile, (err, data) => {
77 | const reData = data.toString().replace(from, to);
78 | fs.writeFile(indexHTMLFile, reData);
79 | });
80 | }
81 |
82 | // read needed data and create the final production index.html
83 | async pack() {
84 | console.log('>>> PACK HTML (base64 encoded fonts in css file, base64 encoded css file as inline data source)');
85 | this.cssFile = await fs.readFile(this.cssFileName);
86 | this.jsFile = await fs.readFile(this.jsFileName);
87 | this.htmlFile = await fs.readFile(this.htmlFileNameFrom);
88 | this.faviconFile = await fs.readFile(this.faviconFileName);
89 |
90 | await this.packFonts();
91 | await this.packHTML();
92 | }
93 |
94 | // replace font urls in the css file with base64 encoded font data
95 | packFonts() {
96 | this.package.fonts = [];
97 | const promises = [];
98 |
99 | const find = new RegExp(/\burl\([^)]+.(woff|woff2)"\)/gi);
100 |
101 | const matches = `${this.cssFile}`.match(find);
102 | matches.forEach(m => {
103 | const cssPath = `${m.replace(`url("./`, '').replace(`")`, '')}`;
104 | const filePath = `${this.cssRoot}/${cssPath}`;
105 | const fileRead = fs.readFile(filePath).then(data => {
106 | const fullData = {
107 | cssPath: cssPath,
108 | cssString: `url("./${cssPath}")`,
109 | filePath: filePath,
110 | data: data.toString('base64')
111 | }
112 | return Promise.resolve(fullData);
113 | });
114 | promises.push(fileRead);
115 | });
116 |
117 | return Promise.all(promises)
118 | .then(files => files.forEach(file => this.package.fonts.push(file)))
119 | .then(() => {
120 | this.package.fonts.forEach(font => this.cssFile = `${this.cssFile}`.replace(font.cssString, `url("data:text/css;base64,${font.data}")`));
121 | return fs.writeFile(this.cssFileName, this.cssFile);
122 | });
123 | }
124 |
125 | // replace all the things in the html file
126 | packHTML() {
127 | const cssFile = `${Buffer.from(this.cssFile).toString('base64')}`;
128 | const faviconFile = `${this.faviconFile.toString('base64')}`; // not from buffer, because the read results a buffer
129 |
130 | this.htmlFile = `${this.htmlFile}`
131 | .replace('', ``)
132 | .replace('', ``)
133 | .replace('', ``)
134 |
135 | //.replace('', ``)
136 | //.replace('', ``)
137 | ;
138 |
139 | return fs.writeFile(this.htmlFileNameTo, this.htmlFile);
140 | }
141 |
142 | // write a version file with the compilation hash from webpack
143 | writeHash(hash) {
144 | return fs.writeFile(`${this.docRoot}/version`, this.hash || hash);
145 | }
146 |
147 | // delete unused files from prod folder
148 | async clean() {
149 | const proms = [];
150 |
151 | // move the app.js from js folder to doc root
152 | await fs.move(`${this.docRoot}/js/app.js`, `${this.docRoot}/app.js`);
153 |
154 | const files = ['dev.html', 'index_template.html', 'favicon.ico', 'css', 'js'];
155 | files.forEach(file => proms.push(fs.remove(`${this.docRoot}/${file}`)))
156 | return Promise.all(proms);
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/frontend/dist/prod/version:
--------------------------------------------------------------------------------
1 | 444213de7856819f27b9
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "raspiscan_frontend",
3 | "version": "0.0.1",
4 | "description": "simple ui to map devices on mqtt chanels",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "node --experimental-modules --experimental-json-modules config/WebpackConfigDev.js",
9 | "build": "node --experimental-modules --experimental-json-modules config/WebpackConfigProd.js"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@glidejs/glide": "^3.6.0",
15 | "ajv": "^8.12.0",
16 | "browser-sync": "^2.27.11",
17 | "clean-webpack-plugin": "^4.0.0",
18 | "copy-webpack-plugin": "^11.0.0",
19 | "css-loader": "^6.7.3",
20 | "es6-templates": "^0.2.3",
21 | "eslint": "^8.33.0",
22 | "eslint-loader": "^3.0.3",
23 | "eslint-webpack-plugin": "^4.0.0",
24 | "extract-loader": "^5.1.0",
25 | "file-loader": "^6.2.0",
26 | "fs-extra": "^11.1.0",
27 | "html-webpack-plugin": "^5.5.0",
28 | "http-proxy-middleware": "^2.0.6",
29 | "i18n-js": "^4.2.3",
30 | "node-sass": "^8.0.0",
31 | "postcss-loader": "^7.0.2",
32 | "postcss-preset-env": "^8.0.1",
33 | "sass-loader": "^13.2.0",
34 | "style-loader": "^3.3.1",
35 | "stylelint": "^14.16.1",
36 | "stylelint-config-standard-scss": "^6.1.0",
37 | "stylelint-webpack-plugin": "^4.0.0",
38 | "swiper": "^9.0.5",
39 | "template-literals-loader": "^1.1.2",
40 | "terser-webpack-plugin": "^5.3.6",
41 | "webpack": "^5.75.0",
42 | "webpack-bundle-analyzer": "^4.7.0",
43 | "webpack-cli": "^5.0.1",
44 | "webpack-dev-server": "^4.11.1",
45 | "webpack-merge": "^5.8.0",
46 | "webpack-stats-plugin": "^1.1.1"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-100.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-100.woff2
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-200.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-200.woff2
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-300.woff2
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-500.woff2
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-600.woff2
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-700.woff2
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-800.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-800.woff2
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-900.woff2
--------------------------------------------------------------------------------
/frontend/public/css/fonts/Barlow/barlow-v12-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/css/fonts/Barlow/barlow-v12-latin-regular.woff2
--------------------------------------------------------------------------------
/frontend/public/dev.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | raspiscan
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seekwhencer/node-rtl433-ui/964cb51dc3d332a124cac4a1b193c1df7c0399b3/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | raspiscan
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/public/index_template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | raspiscan
6 |
7 |
8 |
9 |
10 |
11 |
12 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Author: Matthias Kallenbach, Berlin 2023
3 | *
4 | */
5 | import Main from './lib/Main.js';
6 | window.APP = Main;
7 |
--------------------------------------------------------------------------------
/frontend/src/lib/Global/Events.js:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'events';
2 |
3 | export default class Events {
4 | constructor(parent, options) {
5 | this.event = new EventEmitter();
6 | }
7 |
8 | on() {
9 | this.event.on.apply(this.event, Array.from(arguments));
10 | }
11 |
12 | emit() {
13 | this.event.emit.apply(this.event, Array.from(arguments));
14 | }
15 |
16 | removeListener() {
17 | this.event.removeListener.apply(this.event, Array.from(arguments));
18 | }
19 |
20 | removeAllListeners() {
21 | this.event.removeAllListeners.apply(this.event, Array.from(arguments));
22 | }
23 | }
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/lib/Global/Globals.js:
--------------------------------------------------------------------------------
1 | import './Log.js';
2 | import './Utils.js';
3 | import ModuleClass from './Module.js';
4 | window.MODULECLASS = ModuleClass;
5 |
6 |
--------------------------------------------------------------------------------
/frontend/src/lib/Global/Log.js:
--------------------------------------------------------------------------------
1 | export default class Log {
2 | constructor() {
3 | window.LOG_PARENT = console.log;
4 | console.log = this.log;
5 | }
6 |
7 | log() {
8 | if (!window.OPTIONS)
9 | return;
10 |
11 | if (!window.OPTIONS.debug)
12 | return;
13 |
14 | window.LOG_PARENT.apply(this, arguments);
15 | }
16 | }
17 |
18 | window.LOG = new Log().log;
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Global/Module.js:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'events';
2 |
3 | export default class Module {
4 |
5 | constructor(parent, options) {
6 | this.name = 'module';
7 | this.ready = false;
8 | this.options = options;
9 | this.defaults = {};
10 | this.event = new EventEmitter();
11 |
12 | parent ? this.parent = parent : null;
13 | this.parent ? this.parent.app ? this.app = this.parent.app : null : null;
14 | }
15 |
16 | on() {
17 | this.event.on.apply(this.event, Array.from(arguments));
18 | }
19 |
20 | emit() {
21 | this.event.emit.apply(this.event, Array.from(arguments));
22 | }
23 |
24 | removeListener() {
25 | this.event.removeListener.apply(this.event, Array.from(arguments));
26 | }
27 |
28 | removeAllListeners() {
29 | this.event.removeAllListeners.apply(this.event, Array.from(arguments));
30 | }
31 |
32 | get(match, not) {
33 | return this.items.filter(item => {
34 | if (item.id === match) {
35 | return not !== item.id;
36 |
37 | }
38 | })[0];
39 | }
40 |
41 | getF(field, match, not) {
42 | return this.items.filter(item => {
43 | if (item[field] === match) {
44 | return not !== item[field];
45 |
46 | }
47 | })[0];
48 | }
49 |
50 | toDOM(string) {
51 | // @TODO - not the first -> all !!!.
52 | const body = new DOMParser().parseFromString(string, "text/html").documentElement.querySelector('body');
53 |
54 | if (body.children.length > 1) {
55 | return body.children; // is a NodeList
56 | }
57 | return body.firstChild; // is a node
58 | }
59 |
60 | /**
61 | * data could be a child or a HTMLCollection
62 | * @param data
63 | */
64 | append(data, target) {
65 | if (data.length > 1) {
66 | data.forEach(i => target.append(i));
67 | } else {
68 | target.append(data);
69 | }
70 |
71 | return target;
72 |
73 | }
74 |
75 | fetch(url, requestOptions) {
76 | !requestOptions ? requestOptions = {
77 | method: 'GET'
78 | } : null;
79 |
80 | return fetch(url, requestOptions)
81 | .then(response => {
82 | if (!response.ok)
83 | return Promise.reject(response.statusText);
84 |
85 | return response.json();
86 | });
87 | }
88 |
89 | fetchAudio(url, requestOptions) {
90 | !requestOptions ? requestOptions = {
91 | method: 'GET'
92 | } : null;
93 |
94 | return fetch(url, requestOptions)
95 | .then(response => {
96 | if (!response.ok)
97 | return Promise.reject(response.statusText);
98 |
99 | return response.blob();
100 | });
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/frontend/src/lib/Global/Utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Wants a valid html string
3 | * Returns a valid DOM Element Tree
4 | *
5 | * @param string
6 | * @returns {ChildNode}
7 | */
8 | window.toDOM = string => new DOMParser().parseFromString(string, "text/html").documentElement.querySelector('body').firstChild;
9 |
10 | /**
11 | * Wait for n milliseconds
12 | * Returns a promise
13 | *
14 | * @param ms
15 | * @returns {Promise}
16 | */
17 | window.wait = ms => {
18 | return new Promise(resolve => {
19 | setTimeout(() => {
20 | resolve();
21 | }, ms);
22 | });
23 | };
24 |
25 | window.randomInt = (min, max) => {
26 | min = Math.ceil(min);
27 | max = Math.floor(max);
28 | return Math.floor(Math.random() * (max - min)) + min;
29 | };
30 |
31 | window.ksortObjArray = (array, key) => {
32 | const compare = (a, b) => {
33 | let comparison = 0;
34 | if (a[key] > b[key]) {
35 | comparison = 1;
36 | } else if (a[key] < b[key]) {
37 | comparison = -1;
38 | }
39 | return comparison;
40 | };
41 | return array.sort(compare);
42 | };
43 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/Templates/Device.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ID ${id} |
7 | PROTOCOL ${protocol} |
8 | CHANNEL ${channel} |
9 | MODEL
10 | ${model}
11 | |
12 | HASH
13 | ${hash}
14 | |
15 |
16 |
17 |
21 |
25 |
26 | |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ${fields.map(f => `
35 |
36 | ${f}
38 | ${data[f]}
40 | |
41 | `).join('')}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/Templates/DeviceTopics.html:
--------------------------------------------------------------------------------
1 | ${topics.length > 0 ?
2 | `${topics.map(t => `
3 |
4 | ${t.data.field} |
5 |
6 | ${data[t.data.field]}
7 | |
8 | ${t.data.topic} |
9 |
10 | `).join('')}` : ``}
11 |
12 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/Templates/Exclude.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
${field.toUpperCase()}
4 |
${value}
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/Templates/Topic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
${topic}
4 |
${device ? device.hash : ``}
5 |
6 |
${fields.length > 0 ?
7 | `${fields.map(field => `
8 |
9 | ${field} |
10 | ${data[field]}
12 | |
13 |
14 | `).join('')}` : ``}
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/Templates/TopicAdd.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
${model}
4 |
5 |
${hash}
6 |
7 |
${field}
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/Templates/layout.html:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/devices.js:
--------------------------------------------------------------------------------
1 | import Device from './device.js';
2 |
3 | export default class Devices extends MODULECLASS {
4 | constructor(parent, options) {
5 | super(parent, options);
6 | this.label = 'DEVICES';
7 |
8 | this.target = this.parent.target.querySelector('[data-devices]');
9 |
10 | this.dataSource = {};
11 | this.data = new Proxy(this.dataSource, {
12 | get: (target, prop, receiver) => {
13 | return target[prop] || this.dataSource[prop];
14 | },
15 | set: (target, prop, device) => {
16 | // add or update
17 | if (!target[prop]) {
18 | target[prop] = device;
19 | target[prop].draw();
20 | } else {
21 | //Object.keys(device.data).forEach(d => this.data[prop].data[d] = device.data[d]);
22 | this.data[prop].update(device.data);
23 | }
24 | return true;
25 |
26 | }
27 | });
28 |
29 | }
30 |
31 | startInterval() {
32 | if (this.interval)
33 | clearInterval(this.interval);
34 |
35 | this.interval = setInterval(() => this.getAll(), 1000);
36 | }
37 |
38 | getAll() {
39 | if (!this.parent.parent.navigation.refresh) {
40 | return Promise.resolve(false);
41 | }
42 |
43 | return this.fetch(`${this.app.urlBase}/devices`).then(raw => {
44 | this.raw = raw.data;
45 | this.raw.forEach(deviceData => this.addDevice(deviceData));
46 |
47 | //@TODO check if some devices where dropped
48 | this.checkRemoved();
49 |
50 | this.order(this.parent.parent.navigation.orderBy || 'time');
51 | this.emit('complete');
52 | return Promise.resolve(true);
53 | });
54 | }
55 |
56 | addDevice(deviceData) {
57 | const device = new Device(this, deviceData);
58 | this.data[device.data.hash] = device;
59 | }
60 |
61 | order(by) {
62 | const orderValues = Object.keys(this.data).map(hash => `A${this.data[hash].data[by].toString().padStart('100','0')}__${this.data[hash].data.hash}`);
63 |
64 | //LOG(this.label, 'ORDER', by, 'VALUES', orderValues);
65 | orderValues.sort();
66 | orderValues.reverse();
67 |
68 | let i = 0;
69 | orderValues.forEach(value => {
70 | const hash = value.split('__')[1];
71 | const device = this.data[hash];
72 | device.target.style.order = i;
73 | i++;
74 | });
75 | }
76 |
77 | keys() {
78 | return Object.keys(this.data);
79 | }
80 |
81 | checkRemoved() {
82 | this.keys().forEach(hash => this.data[hash].checkRemoved());
83 | }
84 |
85 | removeDevice(device) {
86 | delete this.data[device.data.hash];
87 | }
88 |
89 | get models() {
90 | return this.data.map(device => device.data.model);
91 | }
92 |
93 | set models(val) {
94 | // nothing
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/excludes.js:
--------------------------------------------------------------------------------
1 | import ExcludeTemplate from './Templates/Exclude.html';
2 |
3 | export default class Excludes extends MODULECLASS {
4 | constructor(parent, options) {
5 | super(parent, options);
6 | this.label = 'EXCLUDES';
7 | this.parent = parent;
8 |
9 | this.target = this.parent.target.querySelector('[data-excludes]');
10 | this.target.innerHTML = '';
11 |
12 | this.data = [];
13 | this.getAll();
14 | }
15 |
16 | getAll() {
17 | return this.fetch(`${this.app.urlBase}/excludes`).then(raw => {
18 | this.data = raw.data;
19 | this.draw();
20 | this.emit('complete');
21 | return Promise.resolve(true);
22 | });
23 | }
24 |
25 | draw() {
26 | this.target.innerHTML = '';
27 | this.data.forEach(exclude => {
28 | const data = {};
29 | exclude.model ? data.field = 'model' : null;
30 | exclude.hash ? data.field = 'hash' : null;
31 | data.value = exclude.model || exclude.hash;
32 |
33 | const element = toDOM(ExcludeTemplate({
34 | scope: data
35 | }
36 | ));
37 | element.querySelector('[data-remove-button]').onclick = () => this.removeExclude(data);
38 | this.target.append(element);
39 | });
40 | }
41 |
42 | removeExclude(data) {
43 | LOG(this.label, 'REMOVE', data);
44 |
45 | return this.fetch(`${this.app.urlBase}/exclude/remove`, {
46 | method: 'POST',
47 | headers: {
48 | 'Content-Type': 'application/json'
49 | },
50 | body: JSON.stringify(data)
51 | }).then(response => {
52 | LOG(this.label, 'UPDATED:', response.data, '');
53 |
54 | if (response.data === true) {
55 | this.getAll();
56 | }
57 |
58 | return Promise.resolve(true);
59 | });
60 | }
61 |
62 | show() {
63 | this.target.classList.add('active');
64 | }
65 |
66 | hide() {
67 | this.target.classList.remove('active');
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/index.js:
--------------------------------------------------------------------------------
1 | import Tab from '../Tab.js';
2 | import LayoutTemplate from "./Templates/layout.html";
3 | import Devices from './devices.js';
4 | import Topics from './topics.js';
5 | import Excludes from './excludes.js';
6 |
7 | export default class Home extends Tab {
8 | constructor(parent, options) {
9 | super(parent, options);
10 | this.label = 'HOME'
11 | this.tab = 'home';
12 |
13 | this.target = this.toDOM(LayoutTemplate({
14 | scope: {
15 | icons: this.app.icons
16 | }
17 | }));
18 | this.parent.target.append(this.target);
19 |
20 | this.topics = new Topics(this);
21 | this.devices = new Devices(this);
22 | this.excludes = new Excludes(this);
23 |
24 | this.topics.on('complete', () => this.devices.startInterval());
25 | this.devices.on('complete', () => this.topics.update());
26 | }
27 |
28 | show() {
29 | super.show();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/topic.js:
--------------------------------------------------------------------------------
1 | import TopicTemplate from './Templates/Topic.html';
2 |
3 | export default class Topic extends MODULECLASS {
4 | constructor(parent, options) {
5 | super(parent, options);
6 | this.topics = parent;
7 | this.label = 'TOPIC';
8 |
9 | this.dataSource = options;
10 | this.data = new Proxy(this.dataSource, {
11 | get: (target, prop, receiver) => {
12 | return target[prop] || this.dataSource[prop];
13 | },
14 | set: (target, prop, value) => {
15 | if (target[prop] === value)
16 | return true;
17 |
18 | target[prop] = value;
19 |
20 | this.emit('updated', prop, value);
21 | this.emit(prop, value);
22 |
23 | return true;
24 | }
25 | });
26 | }
27 |
28 | draw() {
29 | const data = {...this.data};
30 | delete data.topic;
31 |
32 | this.target = this.toDOM(TopicTemplate({
33 | scope: {
34 | topic: this.data.topic,
35 | device: this.device.data || false,
36 | icons: this.app.icons,
37 | data: data,
38 | fields: Object.keys(data)
39 | }
40 | }));
41 | this.parent.target.append(this.target);
42 |
43 | this.removeButton = this.target.querySelector('[data-remove-button]');
44 | this.removeButton.onclick = () => this.remove();
45 | }
46 |
47 | keys() {
48 | return Object.keys({...this.data});
49 | }
50 |
51 | update() {
52 | if (!this.device)
53 | return;
54 |
55 | const target = this.target.querySelector('[data-topic-device]');
56 | if (target.innerHTML === this.device.data.hash)
57 | return;
58 |
59 | target.innerHTML = this.device.data.hash;
60 | }
61 |
62 | remove() {
63 | LOG(this.label, 'REMOVE', this.data.topic);
64 |
65 | const postData = {
66 | topic: this.data.topic
67 | }
68 |
69 | return this.fetch(`${this.app.urlBase}/topic/remove`, {
70 | method: 'POST',
71 | headers: {
72 | 'Content-Type': 'application/json'
73 | },
74 | body: JSON.stringify(postData)
75 | }).then(response => {
76 | LOG(this.label, 'UPDATED:', response.data, '');
77 |
78 | if (response.data === true) {
79 | this.topics.removeTopic(this.data.topic);
80 | }
81 |
82 | return Promise.resolve(true);
83 | });
84 | }
85 |
86 | delete() {
87 | this.target.remove();
88 | }
89 |
90 | get devices() {
91 | return this.topics.devices;
92 | }
93 |
94 | set devices(val) {
95 |
96 | }
97 |
98 | get device() {
99 | const t = {...this.data}; // make a copy
100 | // without the topic and the value field
101 | delete t.topic;
102 | delete t.field;
103 | let matchDevice = false;
104 | this.topics.parent.devices.keys().forEach(hash => {
105 | const device = this.topics.parent.devices.data[hash];
106 | let matchA = '', matchB = '';
107 |
108 | Object.keys(t).forEach(key => {
109 | matchA += t[key];
110 | matchB += device.data[key];
111 | });
112 |
113 | if (`${matchA}` === `${matchB}`) {
114 | matchDevice = device;
115 | }
116 | });
117 |
118 | return matchDevice;
119 | }
120 |
121 | set device(val) {
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/frontend/src/lib/Home/topics.js:
--------------------------------------------------------------------------------
1 | import Topic from "./topic.js";
2 | import TopicAddTemplate from './Templates/TopicAdd.html';
3 |
4 |
5 | export default class Topics extends MODULECLASS {
6 | constructor(parent, options) {
7 | super(parent, options);
8 | this.label = 'TOPICS';
9 | this.parent = parent;
10 |
11 | this.target = this.parent.target.querySelector('[data-topics]');
12 | this.target.innerHTML = '';
13 |
14 | this.targetAddTopic = this.parent.target.querySelector('[data-topic-add]');
15 |
16 | this.dataSource = {};
17 | this.data = new Proxy(this.dataSource, {
18 | get: (target, prop, receiver) => {
19 | return target[prop] || this.dataSource[prop];
20 | },
21 | set: (target, prop, topic) => {
22 | // add or update
23 | if (!target[prop]) {
24 | target[prop] = topic;
25 | topic.draw();
26 | } else {
27 | Object.keys(topic.data).forEach(d => this.data[prop].data[d] = topic.data[d]);
28 | }
29 | return true;
30 |
31 | }
32 | });
33 | this.getAll();
34 | }
35 |
36 | getAll() {
37 | return this.fetch(`${this.app.urlBase}/topics`).then(raw => {
38 | this.raw = raw.data;
39 | this.raw.forEach(topicsData => {
40 | const topic = new Topic(this, topicsData);
41 | this.data[topic.data.topic] = topic;
42 | });
43 | this.emit('complete');
44 | return Promise.resolve(true);
45 | });
46 | }
47 |
48 | keys() {
49 | return Object.keys({...this.data});
50 | }
51 |
52 | update() {
53 | this.keys().forEach(topic => this.data[topic].update());
54 | }
55 |
56 | //@TODO
57 | addTopic(deviceHash, field) {
58 | LOG(this.label, 'ADD TOPIC', deviceHash, field);
59 |
60 | const topicData = {
61 | field: field,
62 | model: this.devices.data[deviceHash].data.model,
63 | hash: deviceHash
64 | };
65 |
66 | const targetAddTopic = this.toDOM(TopicAddTemplate({
67 | scope: topicData
68 | }));
69 | this.targetAddTopic.replaceChildren(targetAddTopic);
70 |
71 | const addTopicButton = this.targetAddTopic.querySelector('[data-add-button]');
72 | addTopicButton.onclick = () => this.submitAddTopic(topicData);
73 | }
74 |
75 | removeTopic(topic) {
76 | this.data[topic].delete();
77 | delete this.data[topic];
78 | this.getAll().then(() => {
79 | this.devices.keys().forEach(hash => this.devices.data[hash].drawTopics());
80 | });
81 |
82 | }
83 |
84 | submitAddTopic(data) {
85 | const topic = this.targetAddTopic.querySelector('input').value;
86 | LOG(this.label, 'ADD TOPIC', topic, data);
87 |
88 | const postData = {
89 | topic: topic,
90 | hash: data.hash,
91 | field: data.field
92 | }
93 |
94 | return this.fetch(`${this.app.urlBase}/topic/add`, {
95 | method: 'POST',
96 | headers: {
97 | 'Content-Type': 'application/json'
98 | },
99 | body: JSON.stringify(postData)
100 | }).then(response => {
101 | LOG(this.label, 'UPDATED:', response.data, '');
102 |
103 | if (response.data === true) {
104 | this.getAll().then(() => {
105 | this.devices.keys().forEach(hash => this.devices.data[hash].drawTopics());
106 | this.targetAddTopic.innerHTML = '';
107 | });
108 | }
109 |
110 | return Promise.resolve(true);
111 | });
112 | }
113 |
114 | show() {
115 | this.target.classList.add('active');
116 | this.targetAddTopic.classList.add('active');
117 | }
118 |
119 | hide() {
120 | this.target.classList.remove('active');
121 | this.targetAddTopic.classList.remove('active');
122 | }
123 |
124 | get devices() {
125 | return this.parent.devices;
126 | }
127 |
128 | set devices(val) {
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/book.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/check.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/close.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/eye.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/eye_alt.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/heart.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/home.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Icons from : https://github.com/astrit/css.gg
3 | *
4 | * using here as html template to inline the svg data
5 | */
6 |
7 | import IconMusic from "./music.html";
8 | import IconBook from "./book.html";
9 | import IconPodcast from "./podcast.html";
10 | import IconEye from "./eye.html";
11 | import IconPen from "./pen.html";
12 | import IconHeart from "./heart.html";
13 | import IconMouth from "./mouth.html";
14 | import IconOptions from "./options.html";
15 | import IconUser from "./user.html";
16 | import IconHome from "./home.html";
17 |
18 | import IconPlay from "./play.html";
19 | import IconPause from "./pause.html";
20 | import IconStop from "./stop.html";
21 | import IconSkipPrev from "./skip-prev.html";
22 | import IconSkipNext from "./skip-next.html";
23 | import IconCheck from "./check.html";
24 | import IconClose from "./close.html";
25 | import IconPlus from "./plus.html";
26 |
27 | const music = scope => {
28 | typeof scope === 'undefined' ? scope = {} : null;
29 | return IconMusic({
30 | scope: {
31 | ...scope,
32 | height: scope.height || 24,
33 | width: scope.width || 24
34 | }
35 | });
36 | }
37 |
38 | const book = scope => {
39 | typeof scope === 'undefined' ? scope = {} : null;
40 | return IconBook({
41 | scope: {
42 | ...scope,
43 | height: scope.height || 24,
44 | width: scope.width || 24
45 | }
46 | });
47 | }
48 |
49 | const podcast = scope => {
50 | typeof scope === 'undefined' ? scope = {} : null;
51 | return IconPodcast({
52 | scope: {
53 | ...scope,
54 | height: scope.height || 24,
55 | width: scope.width || 24
56 | }
57 | });
58 | }
59 | const eye = scope => {
60 | typeof scope === 'undefined' ? scope = {} : null;
61 | return IconEye({
62 | scope: {
63 | ...scope,
64 | height: scope.height || 24,
65 | width: scope.width || 24
66 | }
67 | });
68 | }
69 |
70 | const pen = scope => {
71 | typeof scope === 'undefined' ? scope = {} : null;
72 | return IconPen({
73 | scope: {
74 | ...scope,
75 | height: scope.height || 24,
76 | width: scope.width || 24
77 | }
78 | });
79 | }
80 |
81 | const heart = scope => {
82 | typeof scope === 'undefined' ? scope = {} : null;
83 | return IconHeart({
84 | scope: {
85 | ...scope,
86 | height: scope.height || 24,
87 | width: scope.width || 24
88 | }
89 | });
90 | }
91 | const mouth = scope => {
92 | typeof scope === 'undefined' ? scope = {} : null;
93 | return IconMouth({
94 | scope: {
95 | ...scope,
96 | height: scope.height || 24,
97 | width: scope.width || 24
98 | }
99 | });
100 | }
101 | const options = scope => {
102 | typeof scope === 'undefined' ? scope = {} : null;
103 | return IconOptions({
104 | scope: {
105 | ...scope,
106 | height: scope.height || 24,
107 | width: scope.width || 24
108 | }
109 | });
110 | }
111 |
112 | const user = scope => {
113 | typeof scope === 'undefined' ? scope = {} : null;
114 | return IconUser({
115 | scope: {
116 | ...scope,
117 | height: scope.height || 24,
118 | width: scope.width || 24
119 | }
120 | });
121 | }
122 |
123 | const home = scope => {
124 | typeof scope === 'undefined' ? scope = {} : null;
125 | return IconHome({
126 | scope: {
127 | ...scope,
128 | height: scope.height || 24,
129 | width: scope.width || 24
130 | }
131 | });
132 | }
133 |
134 | const play = scope => {
135 | typeof scope === 'undefined' ? scope = {} : null;
136 | return IconPlay({
137 | scope: {
138 | ...scope,
139 | height: scope.height || 24,
140 | width: scope.width || 24
141 | }
142 | });
143 | }
144 |
145 | const pause = scope => {
146 | typeof scope === 'undefined' ? scope = {} : null;
147 | return IconPause({
148 | scope: {
149 | ...scope,
150 | height: scope.height || 24,
151 | width: scope.width || 24
152 | }
153 | });
154 | }
155 |
156 | const stop = scope => {
157 | typeof scope === 'undefined' ? scope = {} : null;
158 | return IconStop({
159 | scope: {
160 | ...scope,
161 | height: scope.height || 24,
162 | width: scope.width || 24
163 | }
164 | });
165 | }
166 |
167 | const skipPrev = scope => {
168 | typeof scope === 'undefined' ? scope = {} : null;
169 | return IconSkipPrev({
170 | scope: {
171 | ...scope,
172 | height: scope.height || 24,
173 | width: scope.width || 24
174 | }
175 | });
176 | }
177 |
178 | const skipNext = scope => {
179 | typeof scope === 'undefined' ? scope = {} : null;
180 | return IconSkipNext({
181 | scope: {
182 | ...scope,
183 | height: scope.height || 24,
184 | width: scope.width || 24
185 | }
186 | });
187 | }
188 |
189 | const check = scope => {
190 | typeof scope === 'undefined' ? scope = {} : null;
191 | return IconCheck({
192 | scope: {
193 | ...scope,
194 | height: scope.height || 24,
195 | width: scope.width || 24
196 | }
197 | });
198 | }
199 |
200 | const close = scope => {
201 | typeof scope === 'undefined' ? scope = {} : null;
202 | return IconClose({
203 | scope: {
204 | ...scope,
205 | height: scope.height || 24,
206 | width: scope.width || 24
207 | }
208 | });
209 | }
210 |
211 | const plus = scope => {
212 | typeof scope === 'undefined' ? scope = {} : null;
213 | return IconPlus({
214 | scope: {
215 | ...scope,
216 | height: scope.height || 24,
217 | width: scope.width || 24
218 | }
219 | });
220 | }
221 |
222 |
223 | export {
224 | home as home,
225 | options as options,
226 | user as user,
227 | music as music,
228 | book as book,
229 | podcast as podcast,
230 | eye as eye,
231 | pen as pen,
232 | heart as heart,
233 | mouth as mouth,
234 | play as play,
235 | pause as pause,
236 | stop as stop,
237 | skipPrev as skipPrev,
238 | skipNext as skipNext,
239 | check as check,
240 | close as close,
241 | plus as plus
242 |
243 | }
244 |
245 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/mouth.html:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/music.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/options.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/pause.html:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/pen.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/play.html:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/plus.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/podcast.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/skip-next.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/skip-prev.html:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/stop.html:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/book.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/check.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/close.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/eye_alt.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/heart.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/home.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/mouth.svg:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/music.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/options.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/pause.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/pen.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/play.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/plus.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/podcast.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/skip-next.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/skip-prev.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/stop.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Icons/user.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/Locale/Translations.js:
--------------------------------------------------------------------------------
1 | import DE from './de.json';
2 | import EN from './en.json';
3 |
4 | export default {
5 | 'de' : DE,
6 | 'en' : EN
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/lib/Locale/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "Hello World": "Hallo Welt",
3 | "Hello": "Hallo",
4 | "Pin": "Pin",
5 | "setup add artist text 1": "Hier eine Spotify-Url zu einem Künstler eingeben. Zum Beispiel: ",
6 | "setup add artist text 2": "https://open.spotify.com/artist/4DZ94rcOtqQo9NtE3DIho5?si=-6MUOsVMQZG7hyaD_6F6xg",
7 | "setup add artist text 3": "Es werden alle Alben des Künstlers und deren Tracks in die lokale Datenbank übertragen. Wärend des Vorgangs werden auch alle Künstler- und Album-Bilder heruntergeladen und zwischengespeichert.",
8 | "setup add artist text 4": "Dieser Vorgang kann je nach Umfang eine Weile dauern. Achtung: es kann gut sein, dass nur eine geringe Anzahl an Künstlern dennoch eine erhebliche Menge an Tacks beinhalten.",
9 | "setup add artist text 5": "Um einen Künstler-Link zu erhalten, gehe in Spotify auf einen Künstler, wähle teilen und Link zu Künstler*in kopieren. Den Link aus der Zwischenablage nun hier in das Feld einfügen (STRG + v)",
10 | "setup add artist text 6": "Konnte ein Vorgang nicht beendet werden, setzt ein Neustart den Vorgang fort. Ist ein Vorgang beendet, erscheint bei den Künstler*innen ein neuer Eintrag mit Bild.",
11 | "Download": "Download",
12 | "Image url placeholder": "Bild-URL",
13 | "Ok": "Ok",
14 | "Save": "abspeichern",
15 | "Edit artist": "Künstler bearbeiten",
16 | "Edit album": "Album bearbeiten",
17 | "icon description book": "Hörbuch",
18 | "icon description check": "Okay",
19 | "icon description close": "Schließen oder entfernen",
20 | "icon description eye": "Unsichtbar oder sichtbar machen",
21 | "icon description heart": "Als Lieblingsalbum markieren",
22 | "icon description home": "Diese Ansicht hier",
23 | "icon description mouth": "Vorlesen",
24 | "icon description music": "Musik",
25 | "icon description options": "Optionen und Einstellungen",
26 | "icon description pause": "Pause",
27 | "icon description pen": "Bearbeiten",
28 | "icon description play": "Play. Titel oder ersten Titel eines Albums abspielen",
29 | "icon description plus": "Hinzufügen",
30 | "icon description podcast": "Podcast",
31 | "icon description skipPrev": "eins zurück",
32 | "icon description skipNext": "eins weiter",
33 | "icon description stop": "stop",
34 | "icon description user": "Figuren und Darsteller",
35 | "add topic": "hinzufügen",
36 | "remove topic": "entfernen",
37 | "exclude device": "Gerät ausschießen",
38 | "exclude model": "Modell ausschießen",
39 | "refresh devices" : "Geräte automatisch aktualisieren",
40 | "topics": "Topics",
41 | "excludes": "Ausschlüsse",
42 | "remove exclude": "entfernen",
43 | "order by": "sortieren",
44 | "time": "Zeit",
45 | "count": "Zähler",
46 | "forget": "vergessen"
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/src/lib/Locale/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Hello World": "Hallo Welt",
3 | "Hello": "Hallo",
4 | "Pin": "Pin",
5 | "setup add artist text 1": "Enter artist's Spotify-url. For example: ",
6 | "setup add artist text 2": "https://open.spotify.com/artist/4DZ94rcOtqQo9NtE3DIho5?si=-6MUOsVMQZG7hyaD_6F6xg",
7 | "setup add artist text 3": "All of the artist's albums and their tracks are transferred to the local database. All artist and album images will also be downloaded and cached during the process.\n\n",
8 | "setup add artist text 4": "Depending on the scope, this process can take a while. Warning: it may well be that only a small number of artists still contain a significant amount of tacks.",
9 | "setup add artist text 5": "To get an artist link, go to an artist in Spotify, select share and copy link to artist. Paste the link from the clipboard into the field here(STRG + v)",
10 | "setup add artist text 6": "If a process could not be completed, a restart continues the process. When a process is complete, a new entry with a picture appears for the artists.",
11 | "Download": "Download",
12 | "Image url placeholder": "Image-URL",
13 | "Ok": "Ok",
14 | "Save": "save",
15 | "Edit artist": "edit artist",
16 | "Edit album": "edit album",
17 | "icon description book": "Audiobook",
18 | "icon description check": "Okay",
19 | "icon description close": "close or remove",
20 | "icon description eye": "make invisible",
21 | "icon description heart": "mark as favorite",
22 | "icon description home": "this view here",
23 | "icon description mouth": "read",
24 | "icon description music": "music",
25 | "icon description options": "options and preferences",
26 | "icon description pause": "Pause",
27 | "icon description pen": "edit",
28 | "icon description play": "Play.",
29 | "icon description plus": "add",
30 | "icon description podcast": "Podcast",
31 | "icon description skipPrev": "one back",
32 | "icon description skipNext": "one forward",
33 | "icon description stop": "stop",
34 | "icon description user": "Figures and artists",
35 | "add topic": "add",
36 | "remove topic": "remove",
37 | "exclude device": "exclude",
38 | "exclude model": "exclude",
39 | "refresh devices": "refresh devices",
40 | "topics": "Topics",
41 | "excludes": "Excludes",
42 | "remove exclude": "remove exclude",
43 | "order by": "order by",
44 | "time": "time",
45 | "count": "count",
46 | "forget": "forget"
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/src/lib/Locale/index.js:
--------------------------------------------------------------------------------
1 | import Translations from './Translations.js';
2 | import {I18n} from "i18n-js";
3 |
4 | export default class Locale extends MODULECLASS {
5 | constructor(parent) {
6 | super(parent);
7 |
8 | this.i18n = new I18n();
9 |
10 | Object.keys(Translations).forEach(locale => this.i18n.store({
11 | [locale]: Translations[locale]
12 | }));
13 |
14 | this.i18n.locale = 'de';
15 |
16 | // map it globally
17 | window._T = (text, options) => this.i18n.t(text, options);
18 | window._L = (to, value) => this.i18n.l(to, value);
19 |
20 | }
21 |
22 | setLocale(key) {
23 | this.i18n.locale = key;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/lib/Main.js:
--------------------------------------------------------------------------------
1 | import './Global/Globals.js';
2 |
3 | import Locale from './Locale/index.js';
4 | import * as Icons from './Icons/index.js';
5 |
6 | import Navigation from './Navigation/index.js';
7 |
8 | // tabs
9 | import Home from './Home/index.js';
10 |
11 | export default class Main extends MODULECLASS {
12 | constructor(options) {
13 | super();
14 |
15 | return new Promise((resolve, reject) => {
16 | this.label = 'APP';
17 | LOG(this.label, 'INIT');
18 |
19 | this.app = this;
20 | this.options = options;
21 |
22 | this.icons = Icons;
23 |
24 | this.mediaBaseUrl = `${window.location.origin}/media`;
25 | this.apiBaseUrl = `${window.location.origin}/api`;
26 | this.wsBaseUrl = `${window.location.host}/live`;
27 | this.urlBase = `${this.apiBaseUrl}`;
28 |
29 | this.rootElement = this.options.target;
30 | this.target = this.rootElement;
31 |
32 | LOG(this.label, 'API BASE URL:', this.urlBase);
33 |
34 | // this class
35 | this.on('ready', () => {
36 | this.showTab('home');
37 | resolve(this)
38 | });
39 |
40 | // on a tab change
41 | this.on('tab', tab => {
42 | // display a tab
43 | this.showTab(tab);
44 |
45 | // dummy
46 | this.emit(`tab-${tab}`);
47 | });
48 |
49 | // things
50 | this.locale = new Locale(this);
51 | this.navigation = new Navigation(this);
52 |
53 | // tabs
54 | this.tabs = {
55 | home: new Home(this)
56 | }
57 |
58 | this.ws = new WebSocket(`ws://${this.wsBaseUrl}`);
59 |
60 | this.ws.onopen = (e) => {
61 | this.ws.send("My name is John");
62 | };
63 |
64 | this.ws.onmessage = (event) => {
65 | LOG(`[message] Data received from server: ${event.data}`);
66 | };
67 |
68 | this.ws.onclose = (event) => {
69 | if (event.wasClean) {
70 | LOG(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
71 | } else {
72 | // e.g. server process killed or network down
73 | // event.code is usually 1006 in this case
74 | LOG('[close] Connection died');
75 | }
76 | };
77 |
78 | this.ws.onerror = (error) => {
79 | LOG(`[error]`);
80 | };
81 |
82 | // finally ;)
83 | this.emit('ready');
84 |
85 | });
86 | }
87 |
88 | showTab(tab) {
89 | this.tabs[tab].show();
90 | }
91 |
92 | }
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/frontend/src/lib/Navigation/Templates/navigation.html:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/frontend/src/lib/Navigation/index.js:
--------------------------------------------------------------------------------
1 | import NavigationTemplate from './Templates/navigation.html';
2 |
3 | export default class Navigation extends MODULECLASS {
4 | constructor(parent, options) {
5 | super(parent, options);
6 | this.label = 'NAVIGATION';
7 | LOG(this.label, 'INIT');
8 |
9 | this.target = this.toDOM(NavigationTemplate({
10 | scope: {}
11 | }));
12 | this.parent.target.append(this.target);
13 |
14 | this.refreshSwitch = this.target.querySelector('[data-navigation-refresh]');
15 | this.refreshSwitch.onclick = () => this.toggleRefresh();
16 |
17 | this.sideSwitch = this.target.querySelector('[data-navigation-side]');
18 | this.sideSwitch.onclick = () => this.toggleSide();
19 |
20 | this.orderSwitch = this.target.querySelector('[data-navigation-order]');
21 | this.orderSwitch.onclick = () => this.toggleOrdering();
22 |
23 | this.forgetSwitch = this.target.querySelector('[data-navigation-forget]');
24 | this.forgetSwitch.onclick = () => this.toggleForget();
25 |
26 | this.refresh = true;
27 | this.forget = true;
28 | }
29 |
30 | toggleRefresh() {
31 | this.refresh ? this.refresh = false : this.refresh = true;
32 | LOG(this.label, 'TOGGLE REFRESH', this.refresh);
33 | }
34 |
35 | toggleSide() {
36 | this.side ? this.side = false : this.side = true;
37 | LOG(this.label, 'TOGGLE SIDE', this.side);
38 |
39 | if (!this.side) {
40 | this.parent.tabs.home.topics.show();
41 | this.parent.tabs.home.excludes.hide();
42 | } else {
43 | this.parent.tabs.home.topics.hide();
44 | this.parent.tabs.home.excludes.show();
45 | }
46 | }
47 |
48 | toggleOrdering() {
49 | this.order ? this.order = false : this.order = true;
50 | LOG(this.label, 'TOGGLE ORDERING', this.order);
51 | }
52 |
53 | toggleForget() {
54 | return this.fetch(`${this.app.urlBase}/devices/forget`).then(raw => this.forget = raw.data);
55 | }
56 |
57 | //// getter 'n setter
58 |
59 | get refresh() {
60 | return this._refresh;
61 | }
62 |
63 | set refresh(val) {
64 | this._refresh = val;
65 | this.refresh ? this.refreshSwitch.classList.add('active') : this.refreshSwitch.classList.remove('active');
66 | }
67 |
68 | get side() {
69 | return this._side;
70 | }
71 |
72 | set side(val) {
73 | this._side = val;
74 | this.side ? this.sideSwitch.classList.add('active') : this.sideSwitch.classList.remove('active');
75 | }
76 |
77 | get order() {
78 | return this._order;
79 | }
80 |
81 | set order(val) {
82 | this._order = val;
83 | this.orderBy = this.order ? 'count' : 'time';
84 | this.parent.tabs.home.devices.order(this.orderBy);
85 | this.order ? this.orderSwitch.classList.add('active') : this.orderSwitch.classList.remove('active');
86 | }
87 |
88 | get forget() {
89 | return this._forget;
90 | }
91 |
92 | set forget(val) {
93 | this._forget = val;
94 | this.forget ? this.forgetSwitch.classList.add('active') : this.forgetSwitch.classList.remove('active');
95 | }
96 |
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/frontend/src/lib/Tab.js:
--------------------------------------------------------------------------------
1 | export default class Tab extends MODULECLASS {
2 | constructor(parent, options) {
3 | super(parent, options);
4 |
5 | this.on('draw', () => {
6 | this.target.classList.add('active');
7 | });
8 | }
9 |
10 | show() {
11 | LOG(this.label, 'SHOW TAB', this.tab);
12 |
13 | this.hideAll();
14 | this.target.classList.add('active');
15 | }
16 |
17 | hideAll() {
18 | // hide all tabs
19 | Object.keys(this.app.tabs).forEach(tab => {
20 | tab !== this.tab ? this.app.tabs[tab].hide() : null;
21 | });
22 | }
23 |
24 | hide() {
25 | this.target.classList.remove('active');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/scss/app.scss:
--------------------------------------------------------------------------------
1 | @import "global/_index.scss";
2 | @import "modules/_index.scss";
3 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/_index.scss:
--------------------------------------------------------------------------------
1 | @import "colors.scss";
2 | @import "typo.scss";
3 | @import "responsive.scss";
4 | @import "base.scss";
5 | @import "tab.scss";
6 | @import "button.scss";
7 | @import "form.scss";
8 | @import "modal.scss";
9 | @import "background.scss";
10 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/background.scss:
--------------------------------------------------------------------------------
1 | .background {
2 | position: fixed;
3 | z-index: 0;
4 | width: 100%;
5 | height: 100%;
6 | background-position: center;
7 | background-size: cover;
8 | filter: blur(3px) saturate(0) contrast(100%);
9 | opacity: 0.3;
10 | mix-blend-mode: overlay;
11 | transform: rotateY(180deg);
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/base.scss:
--------------------------------------------------------------------------------
1 | body,
2 | html {
3 | margin: 0;
4 | color: #ddd;
5 | height: 100%;
6 | width: 100%;
7 | overflow: hidden;
8 | }
9 |
10 | body {
11 | font-size: $fs-m;
12 | }
13 |
14 | html {
15 | background-color: #000;
16 | background-image: linear-gradient(180deg, rgba($color-gray-b, 1) 0%, rgba($color-gray-c, 0.5) 100%);
17 | }
18 |
19 | * {
20 | font-family: $font-family;
21 | }
22 |
23 | ul {
24 | margin: 0;
25 | padding: 0;
26 |
27 | li {
28 | margin: 0;
29 | padding: 0;
30 | list-style: none;
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/button.scss:
--------------------------------------------------------------------------------
1 | button {
2 | border: none;
3 | background-color: rgba(255, 255, 255, 0.6);
4 | font-size: 1em;
5 | line-height: 1em;
6 | margin: 0;
7 | padding: 3px 6px;
8 | border-radius: 3px;
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/colors.scss:
--------------------------------------------------------------------------------
1 | $color-pink: rgb(181, 0, 255);
2 | $color-ocean: #01172f;
3 | $color-terra: #4c212a;
4 | $color-leaf: #008e86;
5 | $color-cloud: #0077bd;
6 | $color-splash: #126df6;
7 | $color-warn: #700;
8 | $color-orange: #b70;
9 | $color-primary: $color-leaf;
10 | $color-text: #999;
11 | $color-bg: #ddd;
12 |
13 | // dark, bright
14 | $color-primary-dark: desaturate(darken($color-primary, 10%), 10%);
15 | $color-primary-darkest: desaturate(darken($color-primary, 30%), 5%);
16 | $color-primary-blackhole: desaturate(darken($color-primary, 50%), 5%);
17 | $color-primary-bright: desaturate(lighten($color-primary, 3%), 3%);
18 |
19 | // ocean
20 | $color-ocean-dark: desaturate(darken($color-ocean, 5%), 5%);
21 | $color-ocean-bright: desaturate(lighten($color-ocean, 3%), 3%);
22 | $color-ocean-dark: desaturate(darken($color-ocean, 5%), 5%);
23 | $color-ocean-bright: desaturate(lighten($color-ocean, 3%), 3%);
24 |
25 | // terra
26 | $color-terra-dark: desaturate(darken($color-terra, 3%), 3%);
27 | $color-terra-bright: desaturate(lighten($color-terra, 3%), 3%);
28 |
29 | // leaf
30 | $color-leaf-dark: desaturate(darken($color-leaf, 3%), 3%);
31 | $color-leaf-bright: desaturate(lighten($color-leaf, 5%), 5%);
32 |
33 | // cloud
34 | $color-cloud-dark: desaturate(darken($color-cloud, 5%), 5%);
35 | $color-cloud-bright: desaturate(lighten($color-cloud, 5%), 5%);
36 |
37 | // splash
38 | $color-splash-dark: desaturate(darken($color-splash, 3%), 3%);
39 | $color-splash-blackhole: desaturate(darken($color-splash, 40%), 5%);
40 | $color-splash-bright: desaturate(lighten($color-splash, 3%), 3%);
41 |
42 | // gray
43 | $color-gray-a: #111;
44 | $color-gray-b: #333;
45 | $color-gray-c: #555;
46 | $color-gray-d: #777;
47 |
48 | // matte
49 | $color-matte-dark: rgba(0, 0, 0, 0.2);
50 | $color-matte-darker: rgba(0, 0, 0, 0.4);
51 | $color-matte-darkest: rgba(0, 0, 0, 0.6);
52 | $color-matte-doom: rgba(0, 0, 0, 0.8);
53 | $color-matte-bright: rgba(255, 255, 255, 0.2);
54 | $color-matte-brighter: rgba(255, 255, 255, 0.4);
55 | $color-matte-brightest: rgba(255, 255, 255, 0.6);
56 | $color-matte-bloom: rgba(255, 255, 255, 0.8);
57 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/form.scss:
--------------------------------------------------------------------------------
1 | input[type=text] {
2 | width: 50vw;
3 | font-size: $fs-l;
4 | line-height: $fs-xl;
5 | border: none;
6 | color: white;
7 | padding: 0 15px;
8 | border-radius: 5px;
9 | position: relative;
10 | background-color: rgba(0, 0, 0, 0.2);
11 |
12 | &:focus {
13 | border: none;
14 | }
15 |
16 | &:focus-visible {
17 | border: none;
18 | outline: 2px solid $color-primary-dark;
19 | background-color: rgba(0, 0, 0, 0.2);
20 | }
21 |
22 | &.update {
23 | background: $color-leaf;
24 | outline: none;
25 | }
26 | }
27 |
28 | .form {
29 | &--element {
30 | &__text {
31 | label {
32 | width: 100%;
33 | display: block;
34 | opacity: 0.5;
35 | }
36 |
37 | input[type=text] {
38 |
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/modal.scss:
--------------------------------------------------------------------------------
1 | .modal {
2 | position: fixed;
3 | z-index: 1000;
4 | width: 100%;
5 | height: 100%;
6 | background-image: linear-gradient(180deg, rgba(2, 0, 36, 0.9) 100%, rgba(181, 0, 255, 0.9) 0%);
7 |
8 | button {
9 | cursor: pointer;
10 |
11 | &[data-button=close] {
12 | position: absolute;
13 | right: 20px;
14 | top: 20px;
15 | background-color: rgba(255, 255, 255, 0.1);
16 |
17 | svg {
18 | width: 50px;
19 | height: 50px;
20 | margin: 10px;
21 | color: white;
22 | }
23 |
24 | &:hover {
25 | background-color: $color-primary;
26 |
27 | svg {
28 | color: white;
29 | }
30 | }
31 | }
32 |
33 | &[data-button=submit] {
34 | position: absolute;
35 | margin: auto;
36 | bottom: 20vh;
37 | font-size: $fs-xl;
38 | line-height: $fs-xl;
39 | font-weight: bold;
40 | background-color: $color-primary-dark;
41 | color: white;
42 | }
43 | }
44 |
45 | &--title {
46 | font-weight: 600;
47 | margin-top: 40px;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/responsive.scss:
--------------------------------------------------------------------------------
1 | $screen-xxs-min: 320px;
2 | $screen-xs-min: 640px;
3 | $screen-sm-min: 768px;
4 | $screen-md-min: 1024px;
5 | $screen-lg-min: 1280px;
6 | $screen-xl-min: 1440px;
7 | $screen-xxs-max: ($screen-xs-min - 1);
8 | $screen-xs-max: ($screen-sm-min - 1);
9 | $screen-sm-max: ($screen-md-min - 1);
10 | $screen-md-max: ($screen-lg-min - 1);
11 | $screen-lg-max: ($screen-xl-min - 1);
12 | $media-tv: "only screen and (max-width: #{$screen-xl-min})";
13 | $media-tv-min: "only screen and (min-width: #{$screen-xl-min})";
14 | $media-desktop: "only screen and (max-width: #{$screen-lg-min})";
15 | $media-desktop-min: "only screen and (min-width: #{$screen-lg-min})";
16 | $media-tablet: "only screen and (max-width: #{$screen-md-min})";
17 | $media-tablet-min: "only screen and (min-width: #{$screen-md-min})";
18 | $media-tablet-min-max: "only screen and (max-width: #{$screen-md-min}) and (min-width: #{$screen-sm-min})";
19 | $media-tablet-landscape: "only screen and (max-width: #{$screen-md-min}) and (min-width: #{$screen-sm-min}) and (orientation: landscape)";
20 | $media-tablet-max: "only screen and (max-width: #{$screen-md-max})";
21 | $media-smartphone: "only screen and (max-width: #{$screen-sm-min})";
22 | $media-smartphone-min: "only screen and (min-width: #{$screen-sm-min})";
23 | $media-phone: "only screen and (max-width: #{$screen-xs-min})";
24 | $media-phone-min: "only screen and (min-width: #{$screen-xs-min})";
25 | $media-watch: "only screen and (max-width: #{$screen-xxs-min})";
26 | $landscape: "and (orientation: landscape)";
27 | $portrait: "and (orientation: portrait)";
28 |
29 | @mixin media-tv() {
30 | @media #{$media-tv} {
31 | @content;
32 | }
33 | }
34 |
35 | @mixin media-tv-min() {
36 | @media #{$media-tv-min} {
37 | @content;
38 | }
39 | }
40 |
41 | @mixin media-desktop() {
42 | @media #{$media-desktop} {
43 | @content;
44 | }
45 | }
46 |
47 | @mixin media-desktop-min() {
48 | @media #{$media-desktop-min} {
49 | @content;
50 | }
51 | }
52 |
53 | @mixin media-tablet() {
54 | @media #{$media-tablet} {
55 | @content;
56 | }
57 | }
58 |
59 | @mixin media-tablet-min() {
60 | @media #{$media-tablet-min} {
61 | @content;
62 | }
63 | }
64 |
65 | @mixin media-tablet-min-max() {
66 | @media #{$media-tablet-min-max} {
67 | @content;
68 | }
69 | }
70 |
71 | @mixin media-tablet-max() {
72 | @media #{$media-tablet-max} {
73 | @content;
74 | }
75 | }
76 |
77 | @mixin media-tablet-landscape() {
78 | @media #{$media-tablet-landscape} {
79 | @content;
80 | }
81 | }
82 |
83 | @mixin media-smartphone() {
84 | @media #{$media-smartphone} {
85 | @content;
86 | }
87 | }
88 |
89 | @mixin media-smartphone-min() {
90 | @media #{$media-smartphone-min} {
91 | @content;
92 | }
93 | }
94 |
95 | @mixin media-phone() {
96 | @media #{$media-phone} {
97 | @content;
98 | }
99 | }
100 |
101 | @mixin media-phone-min() {
102 | @media #{$media-phone-min} {
103 | @content;
104 | }
105 | }
106 |
107 | @mixin media-watch() {
108 | @media #{$media-watch} {
109 | @content;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/tab.scss:
--------------------------------------------------------------------------------
1 | [data-tab] {
2 | transform: translate3d(0, -100vh, 0);
3 | transition: all ease-out 0.5s;
4 | opacity: 0;
5 | position: fixed;
6 | top: 90px;
7 | height: calc(100% - 90px); // minus navigation and player
8 | width: 100%;
9 |
10 | &.active {
11 | opacity: 1;
12 | display: block;
13 | transform: translate3d(0, 0, 0);
14 | transition: all ease-out 0.2s;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/scss/global/typo.scss:
--------------------------------------------------------------------------------
1 | // primary
2 | $font-family-primary: "Barlow";
3 | $font-path-primary: "./fonts/Barlow";
4 |
5 | // secondary
6 | $font-family-secondary: "Oswald";
7 | $font-path-secondary: "./fonts/Oswald";
8 |
9 | // family
10 | $font-family: $font-family-primary;
11 |
12 | // woff2 mixin
13 | @mixin font-face($family, $file-name, $font-path, $weight) {
14 | $filepath: $font-path + "/" + $file-name + ".woff2";
15 |
16 | @font-face {
17 | font-family: "#{$family}";
18 | font-weight: $weight;
19 | font-display: swap;
20 | src: url($filepath) format("woff2");
21 | }
22 | }
23 |
24 | // includes
25 | @include font-face($font-family-primary, "barlow-v12-latin-100", $font-path-primary, 100);
26 | @include font-face($font-family-primary, "barlow-v12-latin-200", $font-path-primary, 200);
27 | @include font-face($font-family-primary, "barlow-v12-latin-300", $font-path-primary, 300);
28 | @include font-face($font-family-primary, "barlow-v12-latin-500", $font-path-primary, 500);
29 | @include font-face($font-family-primary, "barlow-v12-latin-600", $font-path-primary, 600);
30 | @include font-face($font-family-primary, "barlow-v12-latin-700", $font-path-primary, 700);
31 |
32 | // sizes
33 | $fs-xxs: 0.6em;
34 | $fs-xs: 0.7em;
35 | $fs-s: 1em;
36 | $fs-m: 1.25em;
37 | $fs-l: 1.5em;
38 | $fs-xl: 2em;
39 | $fs-xxl: 3em;
40 | $fs-huge: 4em;
41 | $fs-omg: 6em;
42 | $fs-jesus: 8em;
43 |
--------------------------------------------------------------------------------
/frontend/src/scss/modules/_index.scss:
--------------------------------------------------------------------------------
1 | @import "./navigation/_index.scss";
2 | @import "./home/_index.scss";
3 | @import "./devices/_index.scss";
4 | @import "./topics/_index.scss";
5 | @import "./excludes/_index.scss";
6 |
7 |
--------------------------------------------------------------------------------
/frontend/src/scss/modules/devices/_index.scss:
--------------------------------------------------------------------------------
1 | .devices {
2 | width: calc(100% - 500px);
3 | height: 100%;
4 | position: absolute;
5 | display: flex;
6 | flex-direction: row;
7 | flex-wrap: wrap;
8 | overflow-y: auto;
9 | align-content: flex-start;
10 |
11 | &--device {
12 | width: 100%;
13 | display: block;
14 | margin-bottom: 10px;
15 | border-bottom: 1px solid $color-gray-c;
16 |
17 | &__body {
18 | overflow: hidden;
19 | display: block;
20 | background-color: rgba($color-gray-a, 0.1);
21 | padding: 3px;
22 | }
23 |
24 | &__id {
25 | font-size: $fs-xs;
26 | display: block;
27 | overflow: hidden;
28 | background-color: $color-gray-c;
29 | text-shadow: 1px 1px rgba(0, 0, 0, 0.5);
30 |
31 | table {
32 | height: 30px;
33 | width: 100%;
34 | }
35 |
36 | td {
37 | text-align: center;
38 | font-weight: 900;
39 | padding-right: 50px;
40 | position: relative;
41 |
42 | span {
43 | font-weight: 300;
44 | }
45 | }
46 |
47 | thead {
48 | td {
49 | text-align: center;
50 | font-weight: 300;
51 | }
52 | }
53 | }
54 |
55 | &__model {
56 | font-size: $fs-xxs;
57 | padding: 5px;
58 | text-align: center;
59 | }
60 |
61 | &__topics {
62 | padding: 3px;
63 | margin: 5px 0;
64 | width: 500px;
65 | float: left;
66 |
67 | table {
68 | width: 100%;
69 | }
70 |
71 | .devices--device__value {
72 | &.updated {
73 | animation: blink-animation 0.25s infinite;
74 | transition-duration: 0s;
75 |
76 | @keyframes blink-animation {
77 | 0% {
78 | background-color: black;
79 | }
80 |
81 | 50% {
82 | background-color: $color-splash;
83 | }
84 |
85 | 100% {
86 | background-color: $color-splash;
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | &__properties {
94 | overflow-y: auto;
95 | height: 100%;
96 | width: calc(100% - 506px);
97 | border-radius: 5px;
98 |
99 | table {
100 | width: 100%;
101 | }
102 | }
103 |
104 | &__property {
105 | font-size: $fs-xxs;
106 | padding: 3px;
107 |
108 | &:hover {
109 | cursor: pointer;
110 | background-color: $color-orange;
111 | border-radius: 3px;
112 | color: black;
113 | }
114 | }
115 |
116 | &__value {
117 | font-size: $fs-xxs;
118 | transition: all 0.2s ease-in-out;
119 | padding: 2px 4px;
120 | border-radius: 3px;
121 | font-weight: 900;
122 | background-color: rgba($color-gray-a, 0.5);
123 | text-align: center;
124 |
125 | &.updated {
126 | background-color: $color-splash;
127 | transition: all 0.2s ease-in-out;
128 | }
129 | }
130 |
131 | &__field {
132 | font-size: $fs-xxs;
133 | font-weight: 900;
134 | margin-top: 10px;
135 | width: 30%;
136 |
137 | &.updated {
138 | background-color: $color-splash;
139 | transition: all 0.2s ease-in-out;
140 | }
141 | }
142 |
143 | &__topic {
144 | font-size: $fs-xxs;
145 | width: 60%;
146 | }
147 |
148 | &__td-model {
149 | width: 20%;
150 | }
151 |
152 | &__td-hash {
153 | width: 30%;
154 | }
155 |
156 | &__td-options {
157 | width: 30%;
158 | }
159 |
160 | &__options {
161 | position: absolute;
162 | right: 0;
163 | top: 0;
164 | width: 100%;
165 | display: flex;
166 | align-items: center;
167 | justify-content: right;
168 | height: 100%;
169 |
170 | button {
171 | margin: 0 5px 0 0;
172 | cursor: pointer;
173 | background-color: $color-warn;
174 | color: white;
175 | font-size: $fs-s;
176 |
177 | &:hover {
178 | background-color: white;
179 | color: $color-primary-dark;
180 | }
181 | }
182 | }
183 |
184 | &.active {
185 | .devices--device__body {
186 | opacity: 1;
187 | background-color: rgba(white, 0.05);
188 | }
189 |
190 | &:hover {
191 | opacity: 1;
192 |
193 | .devices--device__options {
194 | opacity: 1;
195 | }
196 | }
197 |
198 | .devices--device__options {
199 | opacity: 0.2;
200 | }
201 | }
202 |
203 | &:hover {
204 | .devices--device__body {
205 | background-color: rgba($color-primary, 0.8);
206 | }
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/frontend/src/scss/modules/excludes/_index.scss:
--------------------------------------------------------------------------------
1 | .excludes {
2 | width: 500px;
3 | right: 0;
4 | position: absolute;
5 | height: 100%;
6 | overflow-x: auto;
7 | display: none;
8 |
9 | &.active {
10 | display: block;
11 | }
12 |
13 | &--exclude {
14 | margin-bottom: 5px;
15 | position: relative;
16 |
17 | &__body {
18 | padding: 5px 10px;
19 | background-color: rgba($color-primary-dark, 0.5);
20 | }
21 |
22 | &__field {
23 | font-weight: 300;
24 | font-size: $fs-xs;
25 | }
26 |
27 | &__value {
28 | font-weight: 900;
29 | font-size: $fs-s;
30 | }
31 |
32 | &__button {
33 | font-size: $fs-xxs;
34 | position: absolute;
35 | right: 20px;
36 | bottom: 10px;
37 | padding: 5px 10px;
38 | cursor: pointer;
39 | background-color: rgba($color-warn, 0.6);
40 | color: white;
41 |
42 | &:hover {
43 | background-color: white;
44 | color: $color-primary-dark;
45 | }
46 | }
47 |
48 | &:hover {
49 | background-color: rgba($color-primary, 0.8);
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/src/scss/modules/home/_index.scss:
--------------------------------------------------------------------------------
1 | .home {
2 | &--buttons {
3 | margin-top: 20vh;
4 | text-align: center;
5 | padding: 0 100px;
6 |
7 | &__button {
8 | margin: 0 20px 20px 0;
9 | background-color: rgba($color-primary-dark, 0.4);
10 | cursor: pointer;
11 |
12 | svg {
13 | width: 100px;
14 | height: 100px;
15 | color: white;
16 | }
17 |
18 | &:hover,
19 | &:active &:focus,
20 | &:focus-visible {
21 | background-color: $color-primary;
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/scss/modules/navigation/_index.scss:
--------------------------------------------------------------------------------
1 | .navigation {
2 | position: fixed;
3 | width: 100%;
4 | height: 88px;
5 | z-index: 1000;
6 | background: rgba(0, 0, 0, 0.7);
7 | border-bottom: 2px solid $color-primary-dark;
8 | top: 0;
9 | overflow: hidden;
10 | display: block;
11 |
12 | &--refresh,
13 | &--side,
14 | &--order,
15 | &--forget {
16 | position: absolute;
17 | top: 25px;
18 | left: 25px;
19 | background-color: rgba(white, 0.1);
20 | padding: 2px 10px;
21 | border-radius: 22px;
22 | display: flex;
23 | align-items: center;
24 | justify-content: right;
25 |
26 | &__switch {
27 | position: relative;
28 | width: 50px;
29 | height: 26px;
30 | display: inline-block;
31 | background-color: $color-warn;
32 | border-radius: 50px;
33 | cursor: pointer;
34 | transition: all 0.2s ease-in-out;
35 | margin: 5px;
36 |
37 | &:before {
38 | position: absolute;
39 | content: "";
40 | left: 4px;
41 | top: 4px;
42 | width: 18px;
43 | height: 18px;
44 | background-color: white;
45 | border-radius: 18px;
46 | transition: all 0.2s ease-in-out;
47 | }
48 |
49 | &.active {
50 | background-color: $color-primary;
51 | transition: all 0.2s ease-in-out;
52 |
53 | &:before {
54 | transform: translateX(24px);
55 | transition: all 0.2s ease-in-out;
56 | }
57 | }
58 | }
59 |
60 | &__label {
61 | font-size: $fs-xs;
62 | line-height: 20px;
63 | display: inline-block;
64 | margin: 5px;
65 | }
66 | }
67 |
68 | &--side {
69 | left: inherit;
70 | right: 20px;
71 |
72 | &__switch {
73 | background-color: $color-primary;
74 | }
75 | }
76 |
77 | &--order,
78 | &--forget {
79 | top: 25px;
80 | left: 50%;
81 | transform: translateX(calc(-100% - 20px));
82 |
83 | &__title {
84 | position: absolute;
85 | top: -15px;
86 | left: 50%;
87 | transform: translateX(-50%);
88 | font-size: $fs-xxs;
89 | }
90 |
91 | &__switch {
92 | background-color: $color-primary;
93 | }
94 | }
95 |
96 | &--forget {
97 | transform: translateX(10px);
98 |
99 | &__switch {
100 | background-color: $color-warn;
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/frontend/src/scss/modules/topics/_index.scss:
--------------------------------------------------------------------------------
1 | @import "./topics.scss";
2 | @import "./add.scss";
3 |
--------------------------------------------------------------------------------
/frontend/src/scss/modules/topics/add.scss:
--------------------------------------------------------------------------------
1 | .topic-add {
2 | width: 500px;
3 | right: 0;
4 | position: absolute;
5 | height: 200px;
6 | top: 0;
7 | overflow-x: auto;
8 | background-color: $color-orange;
9 | color: black;
10 | display: none;
11 |
12 | &.active {
13 | display: block;
14 | }
15 |
16 | &--body {
17 | padding: 10px;
18 | }
19 |
20 | &--topic {
21 | display: block;
22 | overflow: hidden;
23 |
24 | input {
25 | font-size: $fs-s;
26 | width: 100%;
27 | padding: 0 10px;
28 | border-radius: 5px;
29 |
30 | &:focus-visible {
31 | background-color: $color-primary-dark;
32 | border-radius: 5px;
33 | }
34 | }
35 | }
36 |
37 | &--device-model {
38 | margin-top: 20px;
39 | }
40 |
41 | &--device-hash {
42 | font-size: $fs-xs;
43 | }
44 |
45 | &--field {
46 | font-size: $fs-l;
47 | font-weight: 900;
48 | }
49 |
50 | &--button {
51 | position: absolute;
52 | right: 20px;
53 | bottom: 20px;
54 | background-color: $color-primary-dark;
55 | color: white;
56 | padding: 10px 20px;
57 | cursor: pointer;
58 |
59 | &:hover {
60 | background-color: $color-primary;
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/frontend/src/scss/modules/topics/topics.scss:
--------------------------------------------------------------------------------
1 | .topics {
2 | width: 500px;
3 | right: 0;
4 | position: absolute;
5 | height: calc(100% - 200px);
6 | top: 200px;
7 | overflow-x: auto;
8 | display: none;
9 |
10 | &.active {
11 | display: block;
12 | }
13 |
14 | &--topic {
15 | margin-bottom: 5px;
16 | position: relative;
17 |
18 | &__body {
19 | padding: 5px;
20 | background-color: rgba($color-primary-dark, 0.5);
21 | }
22 |
23 | &__topic {
24 | padding: 0 5px;
25 | font-weight: 900;
26 | font-size: $fs-xs;
27 | }
28 |
29 | &__device {
30 | padding: 0 5px;
31 | font-size: $fs-xxs;
32 | font-weight: 300;
33 | color: rgba(white, 0.3);
34 | }
35 |
36 | &__fields {
37 | padding: 5px;
38 | font-size: $fs-xxs;
39 | }
40 |
41 | &__field {
42 | font-weight: 400;
43 | }
44 |
45 | &__value {
46 | font-weight: 900;
47 | }
48 |
49 | &__button {
50 | font-size: $fs-xxs;
51 | position: absolute;
52 | right: 20px;
53 | bottom: 20px;
54 | padding: 5px 10px;
55 | cursor: pointer;
56 | background-color: rgba($color-warn, 0.6);
57 | color: white;
58 |
59 | &:hover {
60 | background-color: white;
61 | color: $color-primary-dark;
62 | }
63 | }
64 |
65 | &:hover {
66 | background-color: rgba($color-primary, 0.8);
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/rtl_433/rtl-sdr.rules:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2012-2013 Osmocom rtl-sdr project
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 | #
17 |
18 | # original RTL2832U vid/pid (hama nano, for example)
19 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2832", MODE:="0666"
20 |
21 | # RTL2832U OEM vid/pid, e.g. ezcap EzTV668 (E4000), Newsky TV28T (E4000/R820T) etc.
22 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", MODE:="0666"
23 |
24 | # DigitalNow Quad DVB-T PCI-E card (4x FC0012?)
25 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0413", ATTRS{idProduct}=="6680", MODE:="0666"
26 |
27 | # Leadtek WinFast DTV Dongle mini D (FC0012)
28 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0413", ATTRS{idProduct}=="6f0f", MODE:="0666"
29 |
30 | # Genius TVGo DVB-T03 USB dongle (Ver. B)
31 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0458", ATTRS{idProduct}=="707f", MODE:="0666"
32 |
33 | # Terratec Cinergy T Stick Black (rev 1) (FC0012)
34 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00a9", MODE:="0666"
35 |
36 | # Terratec NOXON rev 1 (FC0013)
37 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00b3", MODE:="0666"
38 |
39 | # Terratec Deutschlandradio DAB Stick (FC0013)
40 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00b4", MODE:="0666"
41 |
42 | # Terratec NOXON DAB Stick - Radio Energy (FC0013)
43 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00b5", MODE:="0666"
44 |
45 | # Terratec Media Broadcast DAB Stick (FC0013)
46 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00b7", MODE:="0666"
47 |
48 | # Terratec BR DAB Stick (FC0013)
49 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00b8", MODE:="0666"
50 |
51 | # Terratec WDR DAB Stick (FC0013)
52 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00b9", MODE:="0666"
53 |
54 | # Terratec MuellerVerlag DAB Stick (FC0013)
55 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00c0", MODE:="0666"
56 |
57 | # Terratec Fraunhofer DAB Stick (FC0013)
58 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00c6", MODE:="0666"
59 |
60 | # Terratec Cinergy T Stick RC (Rev.3) (E4000)
61 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00d3", MODE:="0666"
62 |
63 | # Terratec T Stick PLUS (E4000)
64 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00d7", MODE:="0666"
65 |
66 | # Terratec NOXON rev 2 (E4000)
67 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0ccd", ATTRS{idProduct}=="00e0", MODE:="0666"
68 |
69 | # PixelView PV-DT235U(RN) (FC0012)
70 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1554", ATTRS{idProduct}=="5020", MODE:="0666"
71 |
72 | # Astrometa DVB-T/DVB-T2 (R828D)
73 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="15f4", ATTRS{idProduct}=="0131", MODE:="0666"
74 |
75 | # Compro Videomate U620F (E4000)
76 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="185b", ATTRS{idProduct}=="0620", MODE:="0666"
77 |
78 | # Compro Videomate U650F (E4000)
79 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="185b", ATTRS{idProduct}=="0650", MODE:="0666"
80 |
81 | # Compro Videomate U680F (E4000)
82 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="185b", ATTRS{idProduct}=="0680", MODE:="0666"
83 |
84 | # GIGABYTE GT-U7300 (FC0012)
85 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d393", MODE:="0666"
86 |
87 | # DIKOM USB-DVBT HD
88 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d394", MODE:="0666"
89 |
90 | # Peak 102569AGPK (FC0012)
91 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d395", MODE:="0666"
92 |
93 | # KWorld KW-UB450-T USB DVB-T Pico TV (TUA9001)
94 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d397", MODE:="0666"
95 |
96 | # Zaapa ZT-MINDVBZP (FC0012)
97 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d398", MODE:="0666"
98 |
99 | # SVEON STV20 DVB-T USB & FM (FC0012)
100 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d39d", MODE:="0666"
101 |
102 | # Twintech UT-40 (FC0013)
103 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d3a4", MODE:="0666"
104 |
105 | # ASUS U3100MINI_PLUS_V2 (FC0013)
106 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d3a8", MODE:="0666"
107 |
108 | # SVEON STV27 DVB-T USB & FM (FC0013)
109 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d3af", MODE:="0666"
110 |
111 | # SVEON STV21 DVB-T USB & FM
112 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b80", ATTRS{idProduct}=="d3b0", MODE:="0666"
113 |
114 | # Dexatek DK DVB-T Dongle (Logilink VG0002A) (FC2580)
115 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d19", ATTRS{idProduct}=="1101", MODE:="0666"
116 |
117 | # Dexatek DK DVB-T Dongle (MSI DigiVox mini II V3.0)
118 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d19", ATTRS{idProduct}=="1102", MODE:="0666"
119 |
120 | # Dexatek DK 5217 DVB-T Dongle (FC2580)
121 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d19", ATTRS{idProduct}=="1103", MODE:="0666"
122 |
123 | # MSI DigiVox Micro HD (FC2580)
124 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1d19", ATTRS{idProduct}=="1104", MODE:="0666"
125 |
126 | # Sweex DVB-T USB (FC0012)
127 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1f4d", ATTRS{idProduct}=="a803", MODE:="0666"
128 |
129 | # GTek T803 (FC0012)
130 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1f4d", ATTRS{idProduct}=="b803", MODE:="0666"
131 |
132 | # Lifeview LV5TDeluxe (FC0012)
133 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1f4d", ATTRS{idProduct}=="c803", MODE:="0666"
134 |
135 | # MyGica TD312 (FC0012)
136 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1f4d", ATTRS{idProduct}=="d286", MODE:="0666"
137 |
138 | # PROlectrix DV107669 (FC0012)
139 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="1f4d", ATTRS{idProduct}=="d803", MODE:="0666"
140 |
--------------------------------------------------------------------------------
/server/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "node": "16",
8 | "esmodules": true
9 | }
10 | }
11 | ]
12 | ],
13 | "plugins": [
14 | "@babel/plugin-proposal-object-rest-spread",
15 | "@babel/plugin-transform-classes",
16 | "@babel/plugin-transform-runtime",
17 | "@babel/plugin-transform-regenerator",
18 | "@babel/plugin-syntax-import-assertions"
19 | ]
20 | }
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 | config/*.json
2 | config/*.conf
3 | dist
4 | .babelrc
5 | testing.js
6 | webpack-app-pkg.config.js
7 | .env
8 |
9 | !config/*.example
10 | !config/types.json
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16-alpine
2 |
3 | ARG RTL433_RELEASE_TAG=22.11
4 | ARG FRONTEND_COMMIT_ID=d8f7c4cb50388c13046b0f53703009fc5aca31c6
5 |
6 | VOLUME ["/raspiscan/server/node_modules", "/raspiscan/shared/node_modules"]
7 |
8 | RUN apk add --no-cache build-base libtool libusb-dev librtlsdr-dev rtl-sdr cmake git curl
9 |
10 | # get rtl_433 source from release tag
11 | WORKDIR /home/node
12 | RUN curl -L https://github.com/merbanan/rtl_433/archive/refs/tags/${RTL433_RELEASE_TAG}.tar.gz -o rtl_433-${RTL433_RELEASE_TAG}.tar.gz
13 | RUN tar -xvf rtl_433-${RTL433_RELEASE_TAG}.tar.gz
14 |
15 | WORKDIR /home/node/rtl_433-${RTL433_RELEASE_TAG}
16 | RUN mkdir build
17 |
18 | WORKDIR /home/node/rtl_433-${RTL433_RELEASE_TAG}/build
19 | RUN cmake ../
20 | RUN make
21 | RUN make install
22 |
23 | #
24 | WORKDIR /raspiscan/shared
25 | COPY shared/package.json .
26 | RUN npm install
27 | COPY shared .
28 |
29 | WORKDIR /raspiscan/server
30 | COPY server/package.json .
31 | RUN npm install
32 | COPY server .
33 |
34 | COPY server/config/default.conf.example config/default.conf
35 | COPY server/config/mapping.json.example config/mapping.json
36 | COPY server/config/excludes.json.example config/excludes.json
37 |
38 | # obsolete, because there is no polyfill for Proxy()
39 | #RUN npm install pkg -g
40 |
41 | WORKDIR /
42 | RUN chown -R 1000:1000 /root/.npm
43 |
44 | COPY rtl_433/rtl_433.conf /etc/rtl_433/rtl_433.conf
45 | COPY rtl_433/rtl-sdr.rules /etc/udev/rules.d/rtl-sdr.rules
46 |
47 | WORKDIR /usr/local/bin
48 | COPY server/entrypoint.sh .
49 | RUN chmod +x entrypoint.sh
50 | ENTRYPOINT ["entrypoint.sh"]
51 |
52 | WORKDIR /raspiscan/server
53 | RUN mkdir frontend
54 | RUN curl https://github.com/seekwhencer/node-rtl433-ui/blob/${FRONTEND_COMMIT_ID}/index.html -o frontend/index.html
55 | RUN curl https://github.com/seekwhencer/node-rtl433-ui/blob/${FRONTEND_COMMIT_ID}/app.js -o frontend/app.js
56 |
57 | WORKDIR /raspiscan
58 | COPY .env.example .env
59 |
60 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | import './lib/Globals.js';
2 | import Config from '../shared/lib/Config.js';
3 | import WebServer from './lib/Server/index.js';
4 | import Mqtt from './lib/Mqtt/index.js';
5 | import RTL433 from './lib/RTL433/index.js'
6 |
7 | export default class App extends MODULECLASS {
8 | constructor() {
9 | super();
10 |
11 | global.APP = this;
12 |
13 | return new Config(this)
14 | .then(config => {
15 | global.CONF = config;
16 | global.CONFIG = config.configData;
17 | return new WebServer(this);
18 | })
19 | .then(webserver => {
20 | global.APP.WEBSERVER = webserver;
21 | return new Mqtt(this);
22 | })
23 | .then(mqtt => {
24 | global.APP.MQTT = mqtt;
25 | return new RTL433(this);
26 | })
27 | .then(rtl433 => {
28 | global.APP.RTL433 = rtl433;
29 | return Promise.resolve(this);
30 | });
31 | }
32 | }
--------------------------------------------------------------------------------
/server/config/default.conf.example:
--------------------------------------------------------------------------------
1 | # for development
2 | DEBUG=true
3 | # webserver
4 | SERVER_PORT=3000
5 | # secons to forget a unmapped device
6 | DEVICE_UNMAPPED_AGE_MAX=120
7 | # mqtt broker
8 | MQTT_HOST=CHANGE_ME
9 | MQTT_PORT=1883
10 | MQTT_CLIENT_ID=raspiscan
11 |
--------------------------------------------------------------------------------
/server/config/excludes.json.example:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "Toyota"
4 | }
5 | ]
--------------------------------------------------------------------------------
/server/config/mapping.json.example:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "topic" : "sensors/room/sleeping/temperature",
4 | "field" : "temperature_C",
5 | "id" : 94,
6 | "channel" : 1,
7 | "protocol" : 91
8 | },
9 | {
10 | "topic" : "sensors/room/sleeping/humidity",
11 | "field" : "humidity",
12 | "id" : 94,
13 | "channel" : 1,
14 | "protocol" : 91
15 | }
16 | ]
--------------------------------------------------------------------------------
/server/config/types.json:
--------------------------------------------------------------------------------
1 | {
2 | "boolean": [
3 | "DEBUG"
4 | ],
5 | "int": [
6 | "SERVER_PORT",
7 | "MQTT_PORT"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/server/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | # Run command with node if the first argument contains a "-" or is not a system command. The last
5 | # part inside the "{}" is a workaround for the following bug in ash/dash:
6 | # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=874264
7 | if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then
8 | set -- node "$@"
9 | fi
10 |
11 | exec "$@"
12 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import App from './app.js';
2 |
3 | new App().then(() => {
4 | LOG('');
5 | LOG('//////////////////');
6 | LOG('RUNNING:', PACKAGE.name);
7 | LOG('VERSION:', PACKAGE.version);
8 | LOG('ENVIRONMENT:', ENVIRONMENT);
9 | LOG('/////////');
10 | LOG('');
11 | });
12 |
--------------------------------------------------------------------------------
/server/lib/Globals.js:
--------------------------------------------------------------------------------
1 | import '../../shared/lib/Utils.js';
2 | import Module from '../../shared/lib/Module.js';
3 | import Package from '../package.json' assert { type: "json" };
4 | import path from 'path';
5 | import * as Ramda from 'ramda';
6 |
7 | !process.env.DEBUG ? global.DEBUG = true : process.env.DEBUG === 'true' ? global.DEBUG = true : global.DEBUG = false;
8 | process.env.ENVIRONMENT ? global.ENVIRONMENT = process.env.ENVIRONMENT : global.ENVIRONMENT = 'default';
9 | global.APP_DIR = path.resolve(process.env.PWD);
10 |
11 |
12 | console.log(process.env.DEBUG)
13 | import Log from '../../shared/lib/Log.js';
14 |
15 | global.LOG = new Log().log;
16 | global.ERROR = new Log().error;
17 |
18 | process.on('uncaughtException', error => LOG('ERROR:', error));
19 | process.on('SIGINT', () => {
20 | try {
21 | // to some global things here
22 | } catch (e) {
23 | // ...
24 | }
25 | // some graceful exit code
26 | setTimeout(() => {
27 | process.exit(0);
28 | }, 500); // wait 2 seconds
29 | });
30 | process.stdin.resume();
31 |
32 | global.PACKAGE = Package;
33 | global.R = Ramda;
34 | global.MODULECLASS = Module;
35 |
--------------------------------------------------------------------------------
/server/lib/Mqtt/Client.js:
--------------------------------------------------------------------------------
1 | import * as mqtt from "mqtt"
2 |
3 | export default class MqttClient extends MODULECLASS {
4 | constructor(parent) {
5 | super(parent);
6 |
7 | return new Promise((resolve, reject) => {
8 | this.label = 'MQTT CLIENT'
9 | this.url = `mqtt://${MQTT_HOST}:${MQTT_PORT}`;
10 | LOG(this.label, 'INIT ON', this.url, 'AS', `${MQTT_CLIENT_ID}`);
11 |
12 | this.parent = parent;
13 |
14 | this.options = {
15 | connection: {
16 | clientId: `${MQTT_CLIENT_ID}`,
17 | reconnectPeriod: 1000,
18 | connectTimeout: 30 * 1000
19 | //...
20 | }
21 | }
22 |
23 | // events
24 | this.on('connect', () => {
25 | this.publish('app', 'welcome');
26 | LOG(this.label, 'CONNECTED TO:', this.url, 'AS', `${MQTT_CLIENT_ID}`);
27 | resolve(this);
28 | });
29 |
30 | this.on('error', error => {
31 | LOG(this.label, 'ERROR', this.url, 'AS', `${MQTT_CLIENT_ID}`);
32 | ERROR(this.label, error, '');
33 | });
34 |
35 | this.on('message', (topic, buffer) => {
36 | this.message(topic, buffer);
37 | });
38 |
39 | // connect finally
40 | this.connect();
41 | });
42 | }
43 |
44 | connect() {
45 | // connecting
46 | this.connection = mqtt.connect(this.url, this.options.connection);
47 |
48 | // add events
49 | this.connection.on('connect', () => this.emit('connect'));
50 | this.connection.on('reconnect', () => this.emit('reconnect'));
51 | this.connection.on('close', () => this.emit('close'));
52 | this.connection.on('disconnect', (packet) => this.emit('disconnect', packet));
53 | this.connection.on('offline', () => this.emit('offline'));
54 | this.connection.on('error', (error) => this.emit('error', error));
55 | this.connection.on('message', (topic, buffer) => this.emit('message', topic, buffer));
56 |
57 | // subscribe all topics
58 | // this.subscribe('#');
59 | }
60 |
61 | subscribe(topic) {
62 | this.connection.subscribe(topic, err => this.error(err));
63 | }
64 |
65 | publish(topic, data) {
66 | this.connection.publish(topic, data);
67 | }
68 |
69 | message(topic, buffer) {
70 | LOG(this.label, topic.toString(), buffer.toString());
71 | }
72 |
73 | disconnect() {
74 | this.connection.end();
75 | }
76 |
77 | reconnect() {
78 | this.connection.reconnect();
79 | }
80 |
81 | error(err) {
82 | if (err) {
83 | LOG(this.label, err);
84 | }
85 | }
86 |
87 | }
--------------------------------------------------------------------------------
/server/lib/Mqtt/index.js:
--------------------------------------------------------------------------------
1 | import MqttClient from './Client.js';
2 |
3 | export default class Mqtt extends MODULECLASS {
4 | constructor(parent) {
5 | super(parent);
6 |
7 | return new Promise((resolve, reject) => {
8 | this.label = 'MQTT'
9 | LOG(this.label, 'INIT');
10 | this.parent = parent;
11 |
12 | new MqttClient(this)
13 | .then(client => {
14 | this.client = client;
15 | resolve(this);
16 | });
17 | });
18 | }
19 |
20 | publish(topic, data) {
21 | this.client.publish(topic, data);
22 | }
23 |
24 | subscribe(topic) {
25 | this.client.subscribe(topic, err => this.error(err));
26 | }
27 | }
--------------------------------------------------------------------------------
/server/lib/RTL433/Device.js:
--------------------------------------------------------------------------------
1 | import Crypto from 'crypto';
2 | import DeviceTopic from './DeviceTopic.js';
3 |
4 | export default class RTL433Device extends MODULECLASS {
5 | constructor(parent, deviceData) {
6 | super(parent);
7 | this.parent = parent;
8 | this.label = 'RTL433 DEVICE';
9 |
10 | // a device can have multiple topics on different value fields
11 | this.topics = [];
12 |
13 | this.dataSource = deviceData;
14 | this.data = new Proxy(this.dataSource, {
15 | get: (target, prop, receiver) => {
16 | return target[prop] || this.dataSource[prop];
17 | },
18 | set: (target, prop, value) => {
19 | if (target[prop] !== value) {
20 | target[prop] = value;
21 |
22 | // emit the prop
23 | this.emit(prop, value);
24 | }
25 | return true;
26 | }
27 | });
28 |
29 | this.data.hash = `${Crypto.createHash('md5').update(`${this.data.id}${this.data.model}${this.data.channel}${this.data.protocol}${this.data.button}`).digest("hex")}`;
30 | this.time_create = this.data.time;
31 | this.getTopics();
32 | }
33 |
34 | /**
35 | * find all topics for the device
36 | */
37 | getTopics() {
38 | this.removeTopics();
39 |
40 | this.topics = [];
41 | this.topicsMapping.forEach(topic => {
42 | const t = {...topic}; // make a copy
43 |
44 | // without the topic and the value field
45 | delete t.topic;
46 | delete t.field;
47 |
48 | let matchA = '', matchB = '';
49 |
50 | // all other matching definitions like `id`, `protocol` or `channel`
51 | Object.keys(t).forEach(key => {
52 | matchA += t[key];
53 | matchB += this.data[key];
54 | });
55 |
56 | if (`${matchA}` === `${matchB}`) {
57 | const deviceTopic = new DeviceTopic(this, topic);
58 | this.topics.push(deviceTopic);
59 | }
60 | });
61 | }
62 |
63 | /**
64 | * remove the existing event listeners for property update
65 | */
66 | removeTopics() {
67 | this.topics.forEach(topic => {
68 | const field = topic.data.field;
69 |
70 | //const eventNames = this.eventNames();
71 | //const events = this._events(this);
72 | //LOG(this.label, 'EVENTS', eventNames, events, '');
73 |
74 | this.removeAllListeners(field);
75 | this.topics = this.topics.filter(t => topic.data.topic !== t.data.topic);
76 | });
77 | }
78 |
79 | emitInitial() {
80 | // emit on create
81 | Object.keys(this.dataSource).forEach(field => this.emit(field, this.data[field]));
82 | }
83 |
84 | checkExcluded() {
85 | if (this.excludes.hashes.includes(this.data.hash) || this.excludes.models.includes(this.data.model)) {
86 | // remove the device
87 | this.remove();
88 | }
89 | }
90 |
91 | remove() {
92 | this.removeTopics();
93 | Object.keys(this.data).forEach(field => this.removeAllListeners(field));
94 | this.parent.removeDevice(this);
95 | }
96 |
97 | checkAge() {
98 | const create = new Date(this.time_create).getTime();
99 | const now = Date.now();
100 | this.data.age = parseInt((now - create) / 1000);
101 |
102 | if (this.topics.length === 0) {
103 | this.data.livetime = (DEVICE_UNMAPPED_AGE_MAX || 120) - this.data.age;
104 | } else {
105 | this.data.livetime = 'always';
106 | }
107 |
108 | if (this.data.livetime < 0)
109 | this.data.livetime = 0;
110 |
111 | if (this.data.livetime === 0 && this.parent.forget)
112 | this.remove();
113 | }
114 |
115 | get topicsMapping() {
116 | return this.parent.topics.data;
117 | }
118 |
119 | set topicsMapping(val) {
120 | // nothing
121 | }
122 |
123 | get excludes() {
124 | return this.parent.excludes;
125 | }
126 |
127 | set excludes(val) {
128 | // nothing
129 | }
130 |
131 |
132 | }
--------------------------------------------------------------------------------
/server/lib/RTL433/DeviceTopic.js:
--------------------------------------------------------------------------------
1 | export default class RTL433DeviceTopic extends MODULECLASS {
2 | constructor(parent, topicData) {
3 | super(parent);
4 | this.label = 'RTL433 DEVICE TOPIC';
5 | this.device = parent;
6 |
7 | this.dataSource = topicData;
8 | this.data = new Proxy(this.dataSource, {
9 | get: (target, prop, receiver) => {
10 | return target[prop] || this.dataSource[prop];
11 | },
12 | set: (target, prop, value) => {
13 | target[prop] = value;
14 | return true;
15 | }
16 | });
17 |
18 | this.device.on(this.data.field, value => this.onProperty(value));
19 | }
20 |
21 | onProperty(value) {
22 |
23 | SERVER_LOG_TOPICS ? LOG(this.label, 'GOT', value, this.data.field, this.data.topic, JSON.stringify(this.device.data)) : null;
24 | this.publish(value);
25 |
26 | /*APP.WEBSERVER.sendWS(JSON.stringify({
27 | hash: this.device.data.hash,
28 | topic: this.data.topic,
29 | field: this.data.field,
30 | value: value
31 | }));*/
32 | }
33 |
34 | publish(value) {
35 | APP.MQTT.publish(this.data.topic, `${value}`);
36 | }
37 | }
--------------------------------------------------------------------------------
/server/lib/RTL433/Excludes.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 | import path from 'path';
3 |
4 | export default class RTL433Excludes extends MODULECLASS {
5 | constructor(parent, options) {
6 | super(parent);
7 | this.parent = parent;
8 |
9 | this.label = 'RTL433 EXCLUDES';
10 | LOG(this.label, 'INIT');
11 |
12 | this.excludesFile = path.resolve(`${CONF.path}/excludes.json`);
13 | this.data = [];
14 |
15 | this.load();
16 | }
17 |
18 | load() {
19 | LOG(this.label, 'LOAD', this.excludesFile);
20 | return fs.readFile(this.excludesFile, (err, data) => this.data = JSON.parse(data.toString()));
21 | }
22 |
23 | write() {
24 | LOG(this.label, 'WRITE', this.excludesFile);
25 | return fs.writeFile(this.excludesFile, JSON.stringify(this.data), (err, data) => {
26 | });
27 | }
28 |
29 | add(data) {
30 | return new Promise((resolve, reject) => {
31 | if (data.model)
32 | if (this.models.includes(data.model)) {
33 | resolve(false);
34 | return;
35 | }
36 |
37 | if (data.hash)
38 | if (this.hashes.includes(data.hash)) {
39 | resolve(false);
40 | return;
41 | }
42 |
43 | const newExclude = {
44 | model: data.model,
45 | hash: data.hash
46 | }
47 | this.data.push(newExclude);
48 | this.write();
49 |
50 | resolve(true);
51 | });
52 | }
53 |
54 | remove(data) {
55 | return new Promise((resolve, reject) => {
56 | LOG(this.label, 'REMOVE', data, '');
57 |
58 | this.data = this.data.filter(ex => ex[data.field] !== data.value);
59 | this.write();
60 |
61 | resolve(true);
62 | });
63 | }
64 |
65 | contains(device) {
66 | return this.models.includes(device.data.model) || this.hashes.includes(device.data.hash);
67 | }
68 |
69 | get models() {
70 | return this.data.map(ex => ex.model) || [];
71 | }
72 |
73 | set models(val) {
74 | // nothing
75 | }
76 |
77 | get hashes() {
78 | return this.data.map(ex => ex.hash) || [];
79 | }
80 |
81 | set hashes(val) {
82 | // nothing
83 | }
84 | }
--------------------------------------------------------------------------------
/server/lib/RTL433/Topics.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 | import path from 'path';
3 |
4 | export default class RTL433Topics extends MODULECLASS {
5 | constructor(parent, options) {
6 | super(parent);
7 | this.parent = parent;
8 |
9 | this.label = 'RTL433 TOPICS';
10 | LOG(this.label, 'INIT');
11 |
12 | this.topicsMappingFile = path.resolve(`${CONF.path}/mapping.json`);
13 | this.data = [];
14 |
15 | this.load();
16 | }
17 |
18 | load() {
19 | LOG(this.label, 'LOAD', this.topicsMappingFile);
20 | return fs.readFile(this.topicsMappingFile, (err, data) => this.data = JSON.parse(data.toString()));
21 | }
22 |
23 | write() {
24 | LOG(this.label, 'WRITE', this.topicsMappingFile);
25 | return fs.writeFile(this.topicsMappingFile, JSON.stringify(this.data), (err, data) => {
26 | });
27 | }
28 |
29 | add(data) {
30 | return new Promise((resolve, reject) => {
31 | if (`${data.topic}`.trim() === '') {
32 | resolve(false);
33 | return;
34 | }
35 |
36 | const exists = this.data.filter(t => t.topic === data.topic)[0] || false;
37 | if (exists) {
38 | resolve(false);
39 | return;
40 | }
41 |
42 | const newTopic = {
43 | topic: data.topic,
44 | hash: data.hash,
45 | field: data.field
46 | };
47 |
48 | this.data.push(newTopic);
49 | this.write();
50 |
51 | this.parent.devices[data.hash].getTopics();
52 | resolve(true);
53 | });
54 | }
55 |
56 | remove(topic){
57 | return new Promise((resolve, reject) => {
58 | if (`${topic}`.trim() === '') {
59 | resolve(false);
60 | return;
61 | }
62 | const exists = this.data.filter(t => t.topic === topic)[0] || false;
63 | if (!exists) {
64 | resolve(false);
65 | return;
66 | }
67 | this.data = this.data.filter(t => t.topic !== topic);
68 |
69 | // @TODO async
70 | this.write();
71 | this.load();
72 |
73 | this.parent.getTopics();
74 |
75 | resolve(true);
76 | });
77 | }
78 | }
--------------------------------------------------------------------------------
/server/lib/RTL433/index.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs-extra';
2 | import path from 'path';
3 | import {spawn} from 'child_process';
4 | import Device from './Device.js';
5 | import Topics from './Topics.js';
6 | import Excludes from './Excludes.js';
7 |
8 | export default class RTL433 extends MODULECLASS {
9 | constructor(parent) {
10 | super();
11 | return new Promise((resolve, reject) => {
12 | this.debug = DEBUG || false;
13 | this.parent = parent;
14 |
15 | this.label = 'RTL433';
16 | LOG(this.label, 'INIT');
17 |
18 | this.bin = '/usr/local/bin/rtl_433';
19 | this.raw = '';
20 |
21 | this.on('device-added', device => {
22 | // emit initial data "change" for all data properties
23 | device.emitInitial();
24 | device.data.count = 1;
25 | device.checkAge();
26 | //APP.WEBSERVER.sendWS(JSON.stringify(device.data));
27 | });
28 |
29 | this.on('device-updated', device => {
30 | device.data.count++;
31 | device.checkAge();
32 | //console.log();
33 | //console.log(this.label, 'DEVICES UPDATED:', JSON.stringify(device.data));
34 | });
35 |
36 | //
37 | this.topics = new Topics(this);
38 | this.excludes = new Excludes(this);
39 |
40 | this.devicesSource = {};
41 | this.devices = new Proxy(this.devicesSource, {
42 |
43 | get: (target, prop, receiver) => {
44 | return target[prop];
45 | },
46 |
47 | set: (target, prop, device) => {
48 | if (this.excludes.contains(device))
49 | return true;
50 |
51 | if (device.data.model === undefined)
52 | return true;
53 |
54 | if (!Object.keys(this.devices).includes(prop)) { // add if not exists
55 | target[prop] = device;
56 | this.emit('device-added', device);
57 | } else {
58 | Object.keys(device.data).forEach(field => this.devices[prop].data[field] = device.data[field]);
59 | this.emit('device-updated', this.devices[prop]);
60 | }
61 | return true;
62 |
63 | }
64 | });
65 |
66 | this.start();
67 |
68 | // check every second, if an unmapped device is over age
69 | this.forget = true;
70 | this.checkAgeInterval = setInterval(() => this.checkAge(), 1000);
71 |
72 | resolve(this);
73 | });
74 | }
75 |
76 | keys() {
77 | return Object.keys(this.devices);
78 | }
79 |
80 | start() {
81 | const processOptions = ['-F', 'json'];
82 | LOG(this.label, 'STARTING WITH OPTIONS', JSON.stringify(processOptions));
83 | this.process = spawn(this.bin, processOptions);
84 | this.process.stdout.setEncoding('utf8');
85 | this.process.stdout.on('data', chunk => this.parseChunk(chunk));
86 | }
87 |
88 | parseChunk(chunk) {
89 | this.raw += chunk;
90 | const rowsRaw = this.raw.split("\n");
91 | rowsRaw.forEach(rowRaw => {
92 | let row = false;
93 |
94 | try {
95 | row = JSON.parse(rowRaw);
96 | } catch (e) {
97 | //console.log('NOT PARSED:', rowRaw);
98 | }
99 |
100 | if (row) {
101 | // create a new device and try to add if not exists. if exists, it will be updated
102 | const device = new Device(this, row);
103 | this.devices[device.data.hash] = device;
104 |
105 | // extract the parsed row from raw data
106 | this.raw = this.raw.replace(`${rowRaw}\n`, '');
107 | }
108 | });
109 | }
110 |
111 |
112 | reload() {
113 | this.topics.load();
114 | this.excludes.load();
115 | }
116 |
117 | addTopic(data) {
118 | return this.topics.add(data);
119 | }
120 |
121 | removeTopic(topic) {
122 | return this.topics.remove(topic);
123 | }
124 |
125 | getTopics() {
126 | this.keys().forEach(hash => this.devices[hash].getTopics());
127 | }
128 |
129 | addExclude(data) {
130 | return this.excludes.add(data).then(ok => {
131 | this.removeExcluded();
132 | return Promise.resolve(ok);
133 | });
134 | }
135 |
136 | removeExcluded() {
137 | this.keys().forEach(hash => this.devices[hash].checkExcluded());
138 | }
139 |
140 | removeDevice(device) {
141 | SERVER_LOG_TOPICS ? LOG(this.label, 'DELETED', device.data.model, device.data.hash) : null;
142 | delete this.devices[device.data.hash];
143 | }
144 |
145 | removeExclude(data) {
146 | return this.excludes.remove(data);
147 | }
148 |
149 | checkAge() {
150 | this.keys().forEach(hash => this.devices[hash].checkAge());
151 | }
152 |
153 | toggleForget() {
154 | this.forget ? this.forget = false : this.forget = true;
155 | }
156 | }
--------------------------------------------------------------------------------
/server/lib/Server/Route.js:
--------------------------------------------------------------------------------
1 | import bodyParser from 'body-parser';
2 |
3 | export default class Route extends MODULECLASS {
4 | constructor(parent, options) {
5 | super(parent, options);
6 | this.router = EXPRESS.Router();
7 |
8 | this.jsonParser = bodyParser.json();
9 | this.urlencodedParser = bodyParser.urlencoded({ extended: false });
10 | }
11 |
12 | nicePath(path) {
13 | return decodeURI(path).replace(/^\//, '').replace(/\/$/, '');
14 | }
15 |
16 | extractPath(path, subtract) {
17 | return this.nicePath(path).replace(new RegExp(`${subtract}`, ''), '').split('/');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/server/lib/Server/base64.js:
--------------------------------------------------------------------------------
1 | export default 'U2NoZWlzcyBkaWUgV2FuZCBhbg==';
--------------------------------------------------------------------------------
/server/lib/Server/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import path from 'path';
3 | import fs from 'fs-extra';
4 | import expressWs from 'express-ws';
5 | import * as Routes from './routes/index.js';
6 |
7 | export default class WebServer extends MODULECLASS {
8 | constructor(parent) {
9 | super(parent);
10 |
11 | return new Promise((resolve, reject) => {
12 | this.label = 'WEBSERVER';
13 | LOG(this.label, 'INIT');
14 |
15 | this.parent = parent;
16 | this.port = SERVER_PORT || 3000;
17 |
18 | // the websocket connections (not in use)
19 | this.wsConnections = [];
20 |
21 | NODE_ENV === 'production' ? this.env = 'prod' : this.env = 'dev';
22 |
23 | // set the frontend statics source path
24 | this.documentRoot = path.resolve(`${APP_DIR}/${SERVER_FRONTEND_PATH}`);
25 |
26 | // set express globally
27 | global.EXPRESS = express;
28 |
29 | this.create().then(() => resolve(this));
30 |
31 | });
32 | }
33 |
34 | create() {
35 | this.engine = EXPRESS();
36 | this.ws = expressWs(this.engine);
37 |
38 | // websocket connection
39 | this.engine.ws('/live', (ws, req) => {
40 |
41 | this.wsConnections.push(ws);
42 |
43 | ws.on('message', msg => {
44 | LOG(this.label, 'WEBSOCKET MESSAGE INCOME', msg);
45 | //ws.send(msg);
46 | });
47 | });
48 |
49 | // statics
50 | LOG(this.label, 'STATICS FOLDER', this.documentRoot);
51 | this.engine.use(express.static(this.documentRoot));
52 |
53 |
54 | // favicon
55 | this.engine.get('/favicon.ico', (req, res) => {
56 | if (fs.existsSync(icon)) {
57 | res.setHeader('Content-Type', 'application/json');
58 | res.sendFile(icon);
59 | } else {
60 | res.end();
61 | }
62 | });
63 |
64 | // the routes
65 | Object.keys(Routes).forEach(route => this.engine.use(`/api/`, new Routes[route](this)));
66 |
67 | // start
68 | return new Promise((resolve, reject) => {
69 | this.engine.listen(this.port, () => {
70 | LOG(this.label, 'IS LISTENING ON PORT:', this.port);
71 | resolve();
72 | });
73 | });
74 |
75 | }
76 |
77 | sendWS(msg) {
78 | this.wsConnections.forEach(ws => ws.send(msg));
79 | }
80 | }
--------------------------------------------------------------------------------
/server/lib/Server/routes/Device.js:
--------------------------------------------------------------------------------
1 | import Route from '../Route.js';
2 |
3 | export default class extends Route {
4 | constructor(parent, options) {
5 | super(parent, options);
6 |
7 | this.router.post('/device/exclude', this.jsonParser);
8 | this.router.post('/device/exclude', (req, res) => {
9 | const params = req.body;
10 |
11 | APP.RTL433.addExclude(params).then(data => {
12 | res.json({
13 | message: 'exclude device',
14 | data: data
15 | });
16 | });
17 | });
18 |
19 |
20 | return this.router;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/server/lib/Server/routes/Devices.js:
--------------------------------------------------------------------------------
1 | import Route from '../Route.js';
2 |
3 | export default class extends Route {
4 | constructor(parent, options) {
5 | super(parent, options);
6 |
7 | this.router.get('/devices', (req, res) => {
8 | const devices = Object.keys(APP.RTL433.devices).map(hash => {
9 | return {
10 | ...APP.RTL433.devices[hash].data,
11 | topics: APP.RTL433.devices[hash].topics.map(t => {
12 | return {
13 | topic: t.data.topic,
14 | field: t.data.field
15 | }
16 | })
17 | };
18 | });
19 | res.json({
20 | message: 'all devices',
21 | data: devices
22 | });
23 | });
24 |
25 | this.router.get('/devices/reload', (req, res) => {
26 | APP.RTL433.reload();
27 |
28 | res.json({
29 | message: 'reload',
30 | data: {}
31 | });
32 | });
33 |
34 | this.router.get('/devices/forget', (req, res) => {
35 | APP.RTL433.toggleForget();
36 |
37 | res.json({
38 | message: 'reload',
39 | data: APP.RTL433.forget
40 | });
41 | });
42 |
43 | return this.router;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/lib/Server/routes/Exclude.js:
--------------------------------------------------------------------------------
1 | import Route from '../Route.js';
2 |
3 | export default class extends Route {
4 | constructor(parent, options) {
5 | super(parent, options);
6 |
7 | this.router.post('/exclude/remove', this.jsonParser);
8 | this.router.post('/exclude/remove', (req, res) => {
9 | const params = req.body;
10 |
11 | APP.RTL433.removeExclude(params).then(data => {
12 | res.json({
13 | message: 'remve exclude',
14 | data: data
15 | });
16 | });
17 | });
18 |
19 | return this.router;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/server/lib/Server/routes/Excludes.js:
--------------------------------------------------------------------------------
1 | import Route from '../Route.js';
2 |
3 | export default class extends Route {
4 | constructor(parent, options) {
5 | super(parent, options);
6 |
7 | this.router.get('/excludes', (req, res) => {
8 | const topics = APP.RTL433.excludes.data;
9 | res.json({
10 | message: 'all excludes',
11 | data: topics
12 | });
13 | });
14 |
15 | return this.router;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/server/lib/Server/routes/Home.js:
--------------------------------------------------------------------------------
1 | import Route from '../Route.js';
2 |
3 | export default class extends Route {
4 | constructor(parent, options) {
5 | super(parent, options);
6 |
7 | this.router.get('/', (req, res) => {
8 | res.json({
9 | home: "test"
10 | });
11 | });
12 |
13 | return this.router;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/lib/Server/routes/Topic.js:
--------------------------------------------------------------------------------
1 | import Route from '../Route.js';
2 |
3 | export default class extends Route {
4 | constructor(parent, options) {
5 | super(parent, options);
6 |
7 | this.router.post('/topic/add', this.jsonParser);
8 | this.router.post('/topic/add', (req, res) => {
9 | const params = req.body;
10 |
11 | APP.RTL433.addTopic(params).then(data => {
12 | res.json({
13 | message: 'add topic',
14 | data: data
15 | });
16 | });
17 |
18 | });
19 |
20 | this.router.post('/topic/remove', this.jsonParser);
21 | this.router.post('/topic/remove', (req, res) => {
22 | const topic = req.body.topic;
23 |
24 | APP.RTL433.removeTopic(topic).then(data => {
25 | res.json({
26 | message: 'remove topic',
27 | data: data
28 | });
29 | });
30 |
31 | });
32 |
33 | return this.router;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/lib/Server/routes/Topics.js:
--------------------------------------------------------------------------------
1 | import Route from '../Route.js';
2 |
3 | export default class extends Route {
4 | constructor(parent, options) {
5 | super(parent, options);
6 |
7 | this.router.get('/topics', (req, res) => {
8 | const topics = APP.RTL433.topics.data;
9 | res.json({
10 | message: 'all topics',
11 | data: topics
12 | });
13 | });
14 |
15 | return this.router;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/server/lib/Server/routes/index.js:
--------------------------------------------------------------------------------
1 | import HomeRoutes from './Home.js';
2 | import DevicesRoutes from './Devices.js';
3 | import DeviceRoutes from './Device.js';
4 | import TopicRoutes from './Topic.js';
5 | import TopicsRoutes from './Topics.js';
6 | import ExcludeRoutes from './Exclude.js';
7 | import ExcludesRoutes from './Excludes.js';
8 |
9 | export {
10 | HomeRoutes,
11 | DeviceRoutes,
12 | DevicesRoutes,
13 | TopicRoutes,
14 | TopicsRoutes,
15 | ExcludeRoutes,
16 | ExcludesRoutes
17 | };
18 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "raspiscan_server",
3 | "version" : "0.0.1",
4 | "description" : "",
5 | "main" : "index.js",
6 | "type" : "module",
7 | "scripts" : {
8 | "start" : "node --experimental-modules --experimental-json-modules index.js",
9 | "build" : "npm run build:arm",
10 | "babelize" : "node --experimental-modules --experimental-json-modules config/webpack-app-pkg.config.js",
11 | "build:arm" : "npm run babelize && node node_modules/pkg/lib-es5/bin.js dist/app.js --output server-arm64 --targets node16-linux-arm64",
12 | "build:linux" : "npm run babelize && node node_modules/pkg/lib-es5/bin.js dist/app.js --output server-linux64 --targets node14-linux-x64",
13 | "build:win" : "npm run babelize && node node_modules/pkg/lib-es5/bin.js dist/app.js --output server-win64 --targets node14-win-x64"
14 | },
15 | "author" : "",
16 | "license" : "ISC",
17 | "pkg" : {
18 | "scripts" : "dist/app.js",
19 | "assets" : "",
20 | "targets" : [
21 | "node16-alpine-armv7"
22 | ],
23 | "outputPath" : "./"
24 | },
25 | "compilerOptions" : {
26 | "target" : "es2015",
27 | "moduleResolution" : "node"
28 | },
29 | "dependencies" : {
30 | "dateformat" : "^5.0.2",
31 | "dotenv" : "^16.0.1",
32 | "express" : "^4.17.3",
33 | "express-ws" : "^5.0.2",
34 | "form-data" : "^4.0.0",
35 | "fs-extra" : "^10.0.0",
36 | "got" : "^12.0.0",
37 | "mqtt" : "^4.3.7",
38 | "pkg" : "^5.5.1",
39 | "ramda" : "^0.28.0",
40 | "ws" : "^8.3.0"
41 | },
42 | "devDependencies" : {
43 | "@babel/core" : "^7.16.5",
44 | "@babel/plugin-proposal-object-rest-spread" : "^7.16.5",
45 | "@babel/plugin-syntax-top-level-await" : "^7.14.5",
46 | "@babel/plugin-transform-regenerator" : "^7.16.5",
47 | "@babel/plugin-transform-runtime" : "^7.16.5",
48 | "@babel/preset-env" : "^7.16.5",
49 | "babel-loader" : "^8.2.3",
50 | "webpack" : "^5.65.0",
51 | "webpack-cli" : "^4.9.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/server/testing.js:
--------------------------------------------------------------------------------
1 | import {spawn} from 'child_process';
2 |
3 | const bin = '/usr/local/bin/rtl_433';
4 | const processOptions = ['-F', 'json'];
5 | const process = spawn(bin, processOptions);
6 |
7 | let raw = '';
8 | const rows = [];
9 |
10 | process.stdout.setEncoding('utf8');
11 | process.stdout.on('data', chunk => {
12 | raw += chunk;
13 | raw = parseRaw(raw);
14 | console.log('RAW EXTRACTED REST:', raw.length, raw );
15 | });
16 |
17 | const parseRaw = (raw) => {
18 | const rowsRaw = raw.split("\n");
19 |
20 | console.log();
21 | console.log('>>>> PARSE RAW', rowsRaw.length);
22 |
23 | rowsRaw.forEach(rowRaw => {
24 | try {
25 | const row = JSON.parse(rowRaw);
26 | addRow(row);
27 | raw = raw.replace(`${rowRaw}\n`, ''); // extract the parsed row from raw data
28 | } catch (e) {
29 | console.log('NOT PARSED:', rowRaw);
30 | }
31 | });
32 | return raw;
33 | }
34 |
35 | const addRow = (row) => {
36 | console.log('> ROW:', JSON.stringify(row));
37 | }
38 |
--------------------------------------------------------------------------------
/server/webpack-app-pkg.config.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import path from "path";
3 |
4 | const config = {
5 | target: "node",
6 | mode: 'production',
7 | entry: './index.js',
8 | output: {
9 | filename: 'dist/app.js',
10 | path: path.resolve(process.env.PWD),
11 | publicPath: '/',
12 | },
13 | node: {
14 | __dirname: false,
15 | __filename: false
16 | },
17 | experiments: {
18 | topLevelAwait: true,
19 | },
20 | plugins: [
21 | new webpack.DefinePlugin({ "global.GENTLY": false }), // hack for formidable
22 | {
23 | apply: (compiler) => {
24 | compiler.hooks.afterEmit.tap('Complete', (compilation) => {
25 | console.log('>>> BUNDLING COMPLETE');
26 | });
27 | }
28 | }
29 | ],
30 | module: {
31 | rules: [
32 | {
33 | test: /\.js$/,
34 | exclude: /(node_modules|bower_components)/,
35 | use: {
36 | loader: "babel-loader",
37 | options: {
38 | presets: ["@babel/preset-env"],
39 | }
40 | }
41 | }
42 | ]
43 | }
44 | };
45 |
46 | const bundler = webpack(config, (err, stats) => {
47 | if (err || stats.hasErrors()) {
48 | console.log('>>> ERROR: ', err, stats.compilation);
49 | }
50 | });
51 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #
4 | # run this script on the raspberry pi directly
5 | #
6 |
7 | # load .env file
8 | loadConfig() {
9 | DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10 | export $(egrep -v '^#' "${DIR}/.env" | xargs)
11 | }
12 |
13 | # update the host system
14 | update() {
15 | sudo apt-get update -y
16 | sudo apt-get upgrade -y
17 | }
18 |
19 | # docker installation
20 | installDocker() {
21 | sudo apt-get remove docker docker-engine docker.io containerd runc -y
22 | sudo apt-get update -y
23 | sudo curl -sSL https://get.docker.com | sh
24 | sudo usermod -a -G docker pi
25 | }
26 |
27 | installDockerCompose() {
28 | sudo curl -L "${DOCKER_COMPOSE_SOURCE}" -o "/usr/bin/docker-compose"
29 | sudo chmod +x /usr/bin/docker-compose
30 | }
31 |
32 | # create docker volumes
33 | createVolumes() {
34 | echo ""
35 | echo "Creating docker volumes"
36 | echo ""
37 |
38 | docker volume create ${PROJECT_NAME}_tmp
39 | }
40 |
41 |
42 |
43 | #----------------------------------------------------------------------------------------------------------------------
44 |
45 | loadConfig
46 |
47 | echo ""
48 | echo "Update system?"
49 | select yn in "Yes" "No"; do
50 | case $yn in
51 | Yes ) update; break;;
52 | No ) break;;
53 | esac
54 | done
55 |
56 | echo ""
57 | echo "Install Docker?"
58 | select yn in "Yes" "No"; do
59 | case $yn in
60 | Yes ) installDocker; break;;
61 | No ) break;;
62 | esac
63 | done
64 |
65 | echo ""
66 | echo "Install Docker Compose?"
67 | select yn in "Yes" "No"; do
68 | case $yn in
69 | Yes ) installDockerCompose; break;;
70 | No ) break;;
71 | esac
72 | done
73 |
74 | echo ""
75 | echo "Create Docker Volumes?"
76 | select yn in "Yes" "No"; do
77 | case $yn in
78 | Yes ) createVolumes; break;;
79 | No ) break;;
80 | esac
81 | done
82 |
83 | echo ""
--------------------------------------------------------------------------------
/setupBuildx.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #
4 | # run this script on a docker host,
5 | # who can use buildx. not the raspberry pi.
6 | #
7 |
8 | # load .env file
9 | loadConfig() {
10 | DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11 | export $(egrep -v '^#' "${DIR}/.env" | xargs)
12 | }
13 |
14 | setup() {
15 | docker run --rm --privileged multiarch/qemu-user-static:${DOCKER_QEMU_VERSION} --reset -p yes
16 | docker buildx create --name=${DOCKER_BUILDX_NAME}
17 | }
18 |
19 | loadConfig
20 | setup
21 |
--------------------------------------------------------------------------------
/shared/lib/Config.js:
--------------------------------------------------------------------------------
1 | import Module from './Module.js';
2 | import fs from 'fs-extra';
3 | import path from 'path';
4 | import dotenv from 'dotenv';
5 |
6 | export default class Config extends Module {
7 | constructor() {
8 | super();
9 |
10 | return new Promise((resolve, reject) => {
11 | this.label = 'CONFIG';
12 | LOG(this.label, 'INIT');
13 |
14 | try {
15 | this.configDir = SERVER_CONFIG_DIR;
16 | } catch(e) {
17 | this.configDir = 'config';
18 | }
19 |
20 | this.path = path.resolve(`${APP_DIR}/${this.configDir}`);
21 |
22 | this.typesFile = `${this.path}/types.json`;
23 | this.typeDefinitions = fs.readJsonSync(this.typesFile);
24 | this.flattenTypes();
25 |
26 | this.configFile = `${this.path}/${ENVIRONMENT}.conf`;
27 | this.envFile = `${path.resolve(`${APP_DIR}/..`)}/.env`;
28 |
29 | this.on('loaded', () => {
30 | LOG(this.label, 'LOADING COMPLETE', {verbose: 2});
31 | });
32 |
33 | this.data = {
34 | env: {},
35 | envFile: {},
36 | configFile: {}
37 | };
38 |
39 | this.configData = new Proxy({}, {
40 | get: (target, prop, receiver) => {
41 | return this.convertTypeRead(target[prop] || this.data.env[prop] || this.data.configFile[prop] || this.data.envFile[prop], prop);
42 | },
43 | set: (target, prop, value) => {
44 | target[prop] = this.convertTypeWrite(value, prop);
45 | return true;
46 | }
47 | });
48 |
49 | this.load().then(success => {
50 | // map the config data into the global scope
51 | // @TODO - filter allowed
52 | this.properties.forEach(prop => global[prop] = this.configData[prop]);
53 |
54 | resolve(this);
55 | });
56 | });
57 | }
58 |
59 | load() {
60 | this.data.env = process.env;
61 |
62 | return this
63 | .loadConfigFile(this.configFile)
64 | .then(data => {
65 | this.data.configFile = data;
66 | return this.loadConfigFile(this.envFile)
67 | })
68 | .then(data => {
69 | this.data.envFile = data;
70 | return Promise.resolve(true);
71 | });
72 | }
73 |
74 | loadConfigFile(configFile) {
75 | return fs.readFile(configFile)
76 | .then(configData => dotenv.parse(configData))
77 | .catch(err => {
78 | ERROR(this.label, err);
79 | });
80 | }
81 |
82 | reload() {
83 | return this.load();
84 | }
85 |
86 | /**
87 | * expand comma separated values to an array
88 | * @TODO unused
89 | */
90 | expandArrays() {
91 | const envKeys = Object.keys(this.configData);
92 | envKeys.forEach(k => {
93 | const split = this.configData[k].split(',');
94 | if (split.length > 1) {
95 | const arrayData = [];
96 | split.forEach(s => arrayData.push(s.trim()));
97 | this.configData[k] = arrayData;
98 | }
99 | });
100 | }
101 |
102 | flattenTypes() {
103 | this.types = {};
104 | Object.keys(this.typeDefinitions).forEach(type => {
105 | this.typeDefinitions[type].forEach(prop => {
106 | this.types[prop] = type;
107 | });
108 | });
109 | }
110 |
111 | convertTypeRead(value, property) {
112 | if (value === undefined)
113 | return;
114 |
115 | const type = this.types[property];
116 |
117 | if (value.toLowerCase) {
118 | if (value.toLowerCase() === 'true' || value.toLowerCase() === 'yes') {
119 | return true;
120 | }
121 | if (value.toLowerCase() === 'false' || value.toLowerCase() === 'no') {
122 | return false;
123 | }
124 | }
125 |
126 | if (type === 'boolean') {
127 | if (value === '1') {
128 | return true;
129 | }
130 | if (value === '0') {
131 | return false;
132 | }
133 | }
134 |
135 | if (type === 'int')
136 | return parseInt(value);
137 |
138 | return value;
139 | }
140 |
141 | convertTypeWrite(value, property) {
142 | const type = this.types[property];
143 |
144 | if (type === 'boolean') {
145 | if (value === true || value === 'true') {
146 | return '1';
147 | }
148 | if (value === false || value === 'false') {
149 | return '0';
150 | }
151 | }
152 |
153 | if (type === 'int')
154 | return value.toString();
155 |
156 | return value;
157 | }
158 |
159 | get properties() {
160 | const props = [];
161 | Object.keys(this.data).forEach(key => Object.keys(this.data[key]).forEach(prop => !props.includes(prop) ? props.push(prop) : null))
162 | props.sort();
163 | return props;
164 | }
165 |
166 | set properties(val) {
167 |
168 | }
169 |
170 | }
171 |
--------------------------------------------------------------------------------
/shared/lib/Events.js:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'events';
2 |
3 | export default class Events {
4 | constructor(parent, options) {
5 | this.event = new EventEmitter();
6 | }
7 |
8 | on() {
9 | this.event.on.apply(this.event, Array.from(arguments));
10 | }
11 |
12 | emit() {
13 | this.event.emit.apply(this.event, Array.from(arguments));
14 | }
15 |
16 | removeListener() {
17 | this.event.removeListener.apply(this.event, Array.from(arguments));
18 | }
19 |
20 | removeAllListeners() {
21 | this.event.removeAllListeners.apply(this.event, Array.from(arguments));
22 | }
23 |
24 | eventNames() {
25 | return this.event.eventNames.apply(this.event, Array.from(arguments));
26 | }
27 |
28 | _events() {
29 | return this.event._events;
30 | }
31 |
32 | listeners() {
33 | return this.event.listeners.apply(this.event, Array.from(arguments));
34 | }
35 | }
--------------------------------------------------------------------------------
/shared/lib/Log.js:
--------------------------------------------------------------------------------
1 | import dateFormat from 'dateformat';
2 | import Module from './Module.js';
3 |
4 | export default class Log extends Module {
5 | constructor(args) {
6 | super(args);
7 | this.label = 'LOGGER';
8 | }
9 |
10 | log() {
11 | if (global.DEBUG === false) {
12 | return false;
13 | }
14 | let output = [
15 | '[',
16 | dateFormat(new Date(), "H:MM:ss - d.m.yyyy"),
17 | ']'
18 | ].concat(Array.from(arguments));
19 | console.log.apply(console, output);
20 | }
21 |
22 | error() {
23 | if (global.DEBUG === false) {
24 | return false;
25 | }
26 | let output = [
27 | '[',
28 | dateFormat(new Date(), "H:MM:ss - d.m.yyyy"),
29 | ']'
30 | ].concat(Array.from(arguments));
31 | console.error.apply(console, output);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/shared/lib/Module.js:
--------------------------------------------------------------------------------
1 | import Events from './Events.js';
2 | import Crypto from 'crypto';
3 | import {spawn} from 'child_process';
4 |
5 | export default class Module extends Events {
6 | constructor(parent, options) {
7 | super();
8 | this.items = [];
9 | parent ? this.parent = parent : null;
10 | this.parent ? this.parent.app ? this.app = this.parent.app : null : null;
11 | this.id = `${Crypto.createHash('md5').update(`${Date.now()}`).digest("hex")}`; // @TODO random hash
12 | }
13 |
14 | /**
15 | * get one, the first item by:
16 | *
17 | * @param match
18 | * @param field
19 | * @param not
20 | * @returns {*}
21 | */
22 | one(match, field, not) {
23 | return this.get(match, field, not)[0];
24 | }
25 |
26 | /**
27 | * get many items by:
28 | *
29 | * @param match
30 | * @param field
31 | * @param not
32 | * @returns {*[]}
33 | */
34 | many(match, field, not) {
35 | return this.get(match, field, not);
36 | }
37 |
38 | /**
39 | * get some items by:
40 | *
41 | * @param match
42 | * @param field
43 | * @param not
44 | * @returns {*[]}
45 | */
46 | get(match, field, not) {
47 | !field ? field = 'id' : null;
48 | return this.items.filter(item => {
49 | if (item['field'] === match) {
50 | return not !== item['field'];
51 | }
52 | });
53 | }
54 |
55 | /**
56 | *
57 | * @param bin
58 | * @param params
59 | * @returns {Promise}
60 | */
61 | command(bin, params) {
62 | return new Promise((resolve, reject) => {
63 | LOG('MODULE COMMAND()', bin, JSON.stringify(params));
64 | let data = '';
65 | const process = spawn(bin, params);
66 | process.stdout.on('data', chunk => data += chunk);
67 | process.stdout.on('end', () => resolve(data));
68 | });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/shared/lib/Utils.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs from 'fs-extra';
3 |
4 | global.P = (dir, rootDir) => {
5 | if (dir.substring(0, 1) === '/') {
6 | return path.resolve(dir);
7 | } else {
8 | if (rootDir) {
9 | return path.resolve(`${rootDir}/${dir}`);
10 | } else {
11 | return path.resolve(`${APP_DIR}/${dir}`);
12 | }
13 |
14 | }
15 | };
16 |
17 | global.PROP = (target, field, options) => {
18 | Object.defineProperty(target, field, {
19 | ...{
20 | enumerable: false,
21 | configurable: false,
22 | writable: true,
23 | value: false
24 | },
25 | ...options
26 | });
27 | };
28 |
29 | global.FOLDERSYNC = function (folder) {
30 | if (fs.existsSync(folder)) {
31 | const dir = fs.readdirSync(folder);
32 | return dir;
33 | }
34 | };
35 |
36 | global.ksortObjArray = (array, key) => {
37 | const compare = (a, b) => {
38 | let comparison = 0;
39 | if (a[key] > b[key]) {
40 | comparison = 1;
41 | } else if (a[key] < b[key]) {
42 | comparison = -1;
43 | }
44 | return comparison;
45 | };
46 | return array.sort(compare);
47 | };
48 |
--------------------------------------------------------------------------------
/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-rtl433-mqtt-shared",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "dateformat": "^5.0.2",
14 | "dotenv": "^10.0.0",
15 | "exif": "^0.6.0",
16 | "fs-extra": "^10.0.0",
17 | "ini": "^3.0.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/zigbee2mqtt/configuration.yaml.example:
--------------------------------------------------------------------------------
1 | # Let new devices join our zigbee network
2 | permit_join: true
3 |
4 | mqtt:
5 | base_topic: zigbee
6 | server: mqtt://192.168.xxx.xxx:1886
7 | client_id: change_my_name
8 | version: 5
9 |
10 | serial:
11 | port: /dev/ttyUSB0
12 | # adapter: ezsp
13 |
14 | frontend:
15 | port: 7080
16 | host: 0.0.0.0
17 |
18 | advanced:
19 | # network_key: GENERATE
20 | output: attribute
21 | log_level: debug
--------------------------------------------------------------------------------