├── nodes
├── stampzilla-knx
│ ├── RELEASE
│ ├── setup.go
│ ├── config.go
│ └── main.go
├── stampzilla-spc
│ ├── RELEASE
│ └── main_test.go
├── stampzilla-1wire
│ ├── RELEASE
│ ├── config.example.json
│ ├── config.go
│ └── onewire
│ │ └── 1wire.go
├── stampzilla-deconz
│ ├── RELEASE
│ ├── localconfig.json
│ ├── README.md
│ └── config.go
├── stampzilla-enocean
│ ├── RELEASE
│ └── handlersinit.go
├── stampzilla-exoline
│ ├── RELEASE
│ ├── lab
│ │ ├── celltohex
│ │ │ └── celltohex.go
│ │ └── hextocell
│ │ │ └── hextocell.go
│ ├── config.go
│ ├── exoline
│ │ └── exoline_test.go
│ └── config.example.json
├── stampzilla-mbus
│ ├── RELEASE
│ ├── config.go
│ ├── config.example.json
│ └── worker.go
├── stampzilla-modbus
│ ├── RELEASE
│ ├── config.go
│ ├── decode_test.go
│ ├── modbus.go
│ └── config.example.json
├── stampzilla-server
│ ├── RELEASE
│ ├── web
│ │ ├── .eslintignore
│ │ ├── .gitignore
│ │ ├── src
│ │ │ ├── images
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ ├── apple-touch-icon.png
│ │ │ │ ├── mstile-150x150.png
│ │ │ │ ├── android-chrome-192x192.png
│ │ │ │ ├── android-chrome-512x512.png
│ │ │ │ ├── gloomy-forest-background.jpg
│ │ │ │ ├── browserconfig.xml
│ │ │ │ └── site.webmanifest
│ │ │ ├── components
│ │ │ │ ├── editor.scss
│ │ │ │ ├── CustomCheckbox.js
│ │ │ │ ├── SocketModal.js
│ │ │ │ ├── Wrapper.js
│ │ │ │ ├── Card.js
│ │ │ │ ├── FormModal.scss
│ │ │ │ ├── ErrorBoundary.js
│ │ │ │ ├── Link.js
│ │ │ │ ├── Login.js
│ │ │ │ └── Register.js
│ │ │ ├── helpers.js
│ │ │ ├── routes
│ │ │ │ ├── dashboard
│ │ │ │ │ ├── device.scss
│ │ │ │ │ ├── HueColorPicker.js
│ │ │ │ │ └── index.js
│ │ │ │ └── automation
│ │ │ │ │ └── components
│ │ │ │ │ ├── Scene.scss
│ │ │ │ │ ├── SavedStatePicker.scss
│ │ │ │ │ └── StateEditor.js
│ │ │ ├── middlewares
│ │ │ │ ├── toast.js
│ │ │ │ ├── rules.js
│ │ │ │ ├── senders.js
│ │ │ │ ├── schedules.js
│ │ │ │ ├── savedstates.js
│ │ │ │ ├── destinations.js
│ │ │ │ └── persons.js
│ │ │ ├── ducks
│ │ │ │ ├── app.js
│ │ │ │ ├── server.js
│ │ │ │ ├── nodes.js
│ │ │ │ ├── devices.js
│ │ │ │ ├── requests.js
│ │ │ │ ├── connections.js
│ │ │ │ ├── certificates.js
│ │ │ │ ├── index.js
│ │ │ │ ├── savedstates.js
│ │ │ │ ├── persons.js
│ │ │ │ ├── senders.js
│ │ │ │ ├── destinations.js
│ │ │ │ ├── rules.js
│ │ │ │ └── schedules.js
│ │ │ ├── index.html
│ │ │ ├── index.js
│ │ │ └── store.js
│ │ ├── .babelrc
│ │ └── .eslintrc
│ ├── models
│ │ ├── labels.go
│ │ ├── notification
│ │ │ ├── type.go
│ │ │ ├── message.go
│ │ │ ├── pushover
│ │ │ │ └── pushover.go
│ │ │ ├── email
│ │ │ │ ├── email.go
│ │ │ │ └── email_test.go
│ │ │ ├── webhook
│ │ │ │ ├── webhook_test.go
│ │ │ │ └── webhook.go
│ │ │ ├── file
│ │ │ │ ├── file.go
│ │ │ │ └── file_test.go
│ │ │ ├── sender_test.go
│ │ │ ├── wirepusher
│ │ │ │ ├── wirepusher_test.go
│ │ │ │ └── wirepusher.go
│ │ │ └── destination_test.go
│ │ ├── request.go
│ │ ├── readyinfo.go
│ │ ├── serverinfo.go
│ │ ├── node_test.go
│ │ ├── connection.go
│ │ ├── node.go
│ │ ├── message.go
│ │ ├── persons
│ │ │ └── person.go
│ │ └── devices
│ │ │ └── state.go
│ ├── .gitignore
│ ├── config.example.json
│ ├── interfaces
│ │ └── melody.go
│ ├── README.md
│ ├── store
│ │ ├── senders.go
│ │ ├── devices.go
│ │ ├── certificates.go
│ │ ├── server.go
│ │ ├── logic.go
│ │ ├── connections.go
│ │ ├── server_test.go
│ │ ├── destinations.go
│ │ └── request.go
│ ├── handlers
│ │ └── websockethandler.go
│ ├── main.go
│ ├── logic
│ │ ├── rules.json
│ │ └── savedstate.go
│ ├── helpers
│ │ └── isprivateip.go
│ └── websocket
│ │ └── websocket.go
├── stampzilla-tibber
│ ├── RELEASE
│ ├── config.example.json
│ ├── config.go
│ └── tibber_test.go
├── stampzilla-zwave
│ ├── RELEASE
│ └── config.go
├── stampzilla-chromecast
│ ├── RELEASE
│ ├── state.go
│ └── main.go
├── stampzilla-husdata-h60
│ ├── RELEASE
│ ├── config.example.json
│ ├── config.go
│ ├── model_test.go
│ └── main_test.go
├── stampzilla-nx-witness
│ ├── RELEASE
│ ├── config.example.json
│ ├── model_test.go
│ ├── config.go
│ ├── model.go
│ └── api.go
├── stampzilla-google-assistant
│ ├── RELEASE
│ ├── device.go
│ ├── config.go
│ ├── README.md
│ ├── oauthhandler.go
│ └── googleassistant
│ │ ├── request.go
│ │ └── response.go
├── stampzilla-metrics-influxdb
│ └── RELEASE
├── stampzilla-magicmirror
│ ├── web
│ │ ├── .prettierignore
│ │ ├── now.json
│ │ ├── .prettierrc
│ │ ├── renovate.json
│ │ ├── src
│ │ │ ├── setupProxy.js
│ │ │ ├── routes
│ │ │ │ ├── app
│ │ │ │ │ └── index.js
│ │ │ │ └── home
│ │ │ │ │ ├── Clock.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── DeviceList.js
│ │ │ ├── ducks
│ │ │ │ ├── index.js
│ │ │ │ ├── config.js
│ │ │ │ ├── devices.js
│ │ │ │ └── connection.js
│ │ │ ├── index.js
│ │ │ ├── .eslintrc
│ │ │ └── store.js
│ │ ├── .gitignore
│ │ ├── public
│ │ │ ├── manifest.json
│ │ │ └── index.html
│ │ ├── README.md
│ │ ├── LICENSE
│ │ └── package.json
│ ├── config.go
│ └── example-config.json
├── stampzilla-linux
│ ├── RELEASE
│ ├── health.go
│ ├── main.go
│ ├── volume.go
│ └── dpms.go
├── stampzilla-streamdeck
│ ├── fonts
│ │ └── Robotosr.ttf
│ ├── config.go
│ └── keys.go
├── stampzilla-telldus
│ ├── README.md
│ ├── README
│ └── main.c
├── stampzilla-nibe
│ ├── config.go
│ └── nibe
│ │ └── parameters.go
└── stampzilla-keba-p30
│ └── types.go
├── hooks
├── pre-commit
└── install-hooks
├── docs
├── screenshots
│ ├── debug.png
│ ├── nodes.png
│ ├── security.png
│ ├── automation.png
│ └── dashboard.png
├── screenshots.md
├── devices.md
└── README.md
├── pkg
├── hueemulator
│ ├── huestate.go
│ └── request.go
├── runner
│ └── runner.go
├── types
│ ├── duration.go
│ └── duration_test.go
├── installer
│ ├── prepare.go
│ ├── installer.go
│ ├── pidfile.go
│ ├── binary
│ │ └── releases.go
│ └── user.go
├── build
│ └── build.go
└── node
│ └── mdns.go
├── cmd
└── stampzilla
│ └── Makefile
├── .gitignore
├── .travis.yml
├── coverage
├── Makefile
└── README.md
/nodes/stampzilla-knx/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-spc/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-1wire/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-deconz/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-enocean/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-exoline/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-mbus/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-modbus/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-tibber/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-zwave/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-chromecast/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-husdata-h60/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-nx-witness/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-google-assistant/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodes/stampzilla-metrics-influxdb/RELEASE:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | make test
4 |
--------------------------------------------------------------------------------
/nodes/stampzilla-google-assistant/device.go:
--------------------------------------------------------------------------------
1 | package main
2 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/.eslintignore:
--------------------------------------------------------------------------------
1 | src/index.scss
2 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | stats.json
3 |
--------------------------------------------------------------------------------
/nodes/stampzilla-1wire/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "interval":"30s"
3 | }
4 |
--------------------------------------------------------------------------------
/nodes/stampzilla-deconz/localconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "APIKey": "0342FBAE6F"
3 | }
4 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/labels.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Labels map[string]string
4 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "alias": "create-react-app-redux.now.sh"
3 | }
4 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/.gitignore:
--------------------------------------------------------------------------------
1 | *.crt
2 | *.key
3 | vendor
4 | configs
5 |
6 | web/dist
7 | *.json
8 |
--------------------------------------------------------------------------------
/docs/screenshots/debug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/debug.png
--------------------------------------------------------------------------------
/docs/screenshots/nodes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/nodes.png
--------------------------------------------------------------------------------
/nodes/stampzilla-linux/RELEASE:
--------------------------------------------------------------------------------
1 | {
2 | "cgo": true,
3 | "arch": [
4 | "amd64"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/docs/screenshots/security.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/security.png
--------------------------------------------------------------------------------
/nodes/stampzilla-tibber/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "carChargeDuration": "6h",
3 | "token": "asdf"
4 | }
5 |
--------------------------------------------------------------------------------
/docs/screenshots/automation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/automation.png
--------------------------------------------------------------------------------
/docs/screenshots/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/docs/screenshots/dashboard.png
--------------------------------------------------------------------------------
/nodes/stampzilla-husdata-h60/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "interval":"30s",
4 | "host":"http://192.168.13.181/"
5 | }
6 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "jsxBracketSameLine": true
5 | }
--------------------------------------------------------------------------------
/nodes/stampzilla-streamdeck/fonts/Robotosr.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-streamdeck/fonts/Robotosr.ttf
--------------------------------------------------------------------------------
/hooks/install-hooks:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ROOT_FOLDER=`git rev-parse --show-toplevel`
4 |
5 | cp -v $ROOT_FOLDER/hooks/pre-commit $ROOT_FOLDER/.git/hooks
6 |
7 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/favicon.ico
--------------------------------------------------------------------------------
/pkg/hueemulator/huestate.go:
--------------------------------------------------------------------------------
1 | package hueemulator
2 |
3 | type huestate struct {
4 | Handler Handler
5 | // OnState bool
6 | Light *light
7 | Id int
8 | }
9 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": "8080",
3 | "tlsPort": "6443",
4 | "uuid": "serveruuid",
5 | "name": "stampzilla server"
6 | }
7 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/favicon-16x16.png
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/favicon-32x32.png
--------------------------------------------------------------------------------
/nodes/stampzilla-zwave/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Config struct {
4 | Device string `json:"device"`
5 | RecordToFile string `json:"recordToFile"`
6 | }
7 |
--------------------------------------------------------------------------------
/nodes/stampzilla-nx-witness/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "host": "http://1.1.1.1:7001",
3 | "username": "admin",
4 | "password": "asdf",
5 | "interval": "5m"
6 | }
7 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/apple-touch-icon.png
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/mstile-150x150.png
--------------------------------------------------------------------------------
/nodes/stampzilla-1wire/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Config struct {
4 | Interval string
5 | }
6 |
7 | func NewConfig() *Config {
8 | return &Config{}
9 | }
10 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/type.go:
--------------------------------------------------------------------------------
1 | package notification
2 |
3 | type Type string
4 |
5 | const (
6 | TypeMail Type = "mail"
7 | TypeSms Type = "sms"
8 | )
9 |
--------------------------------------------------------------------------------
/pkg/hueemulator/request.go:
--------------------------------------------------------------------------------
1 | package hueemulator
2 |
3 | type Request struct {
4 | UserId string
5 | // RequestedOnState bool
6 | Request *request
7 | RemoteAddr string
8 | }
9 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/editor.scss:
--------------------------------------------------------------------------------
1 | .editor {
2 | padding: 6px;
3 | .node {
4 | color: #000;
5 | background: #eee;
6 | padding: 2px 2px;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/android-chrome-192x192.png
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/android-chrome-512x512.png
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/gloomy-forest-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stampzilla/stampzilla-go/HEAD/nodes/stampzilla-server/web/src/images/gloomy-forest-background.jpg
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "automerge": true,
4 | "major": {
5 | "automerge": false
6 | },
7 | "reviewers": ["notrab"]
8 | }
9 |
--------------------------------------------------------------------------------
/nodes/stampzilla-husdata-h60/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Config struct {
4 | Interval string
5 | Host string
6 | }
7 |
8 | func NewConfig() *Config {
9 | return &Config{}
10 | }
11 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/helpers.js:
--------------------------------------------------------------------------------
1 |
2 | let lastId = 0;
3 | export const uniqeId = (prefix = 'id') => {
4 | lastId += 1;
5 | return `${prefix}${lastId}`;
6 | };
7 |
8 | export default false;
9 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/request.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Request struct {
4 | Version string `json:"version,omitempty"`
5 | Type string `json:"type,omitempty"`
6 | CSR string `json:"csr"`
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/runner/runner.go:
--------------------------------------------------------------------------------
1 | package runner
2 |
3 | type Runner interface {
4 | Start(nodes ...string) error
5 | Stop(nodes ...string) error
6 | Restart(nodes ...string) error
7 | Status() error
8 | Close()
9 | }
10 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/routes/dashboard/device.scss:
--------------------------------------------------------------------------------
1 | .offline {
2 | color: #999;
3 | }
4 |
5 | .device {
6 | div[role="button"] {
7 | cursor: pointer;
8 | border: 0;
9 | outline: none;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/nodes/stampzilla-telldus/README.md:
--------------------------------------------------------------------------------
1 | # telldus
2 |
3 | Connects to local telldusd using telldus C API
4 |
5 | ## Configuration
6 |
7 | telldus requires no extra config except what is configured in /etc/tellstick.conf for telldusd to work.
8 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | const proxy = require("http-proxy-middleware")
2 |
3 | module.exports = app => {
4 | app.use(proxy("/ws", {target: "http://localhost:8089", ws: true}))
5 | app.use(proxy("/proxy", {target: "http://localhost:8089"}))
6 | }
7 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/readyinfo.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/persons"
4 |
5 | type ReadyInfo struct {
6 | Method string `json:"method"`
7 | User *persons.Person `json:"user"`
8 | }
9 |
--------------------------------------------------------------------------------
/nodes/stampzilla-nx-witness/model_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestStampzillaDeviceID(t *testing.T) {
10 | r := Rule{
11 | Comment: "test stampzilla:123a",
12 | }
13 | assert.Equal(t, "123a", r.StampzillaDeviceID())
14 | }
15 |
--------------------------------------------------------------------------------
/nodes/stampzilla-telldus/README:
--------------------------------------------------------------------------------
1 | The telldus c lib is needed for compilation and the search paths is not set correctly by default.
2 |
3 | Here is a dirty workaround
4 |
5 | ln -s /usr/local/include/telldus/telldus-core/client/telldus-core.h /usr/include/telldus-core.h
6 | ln -s /usr/lib/libtelldus-core.so.2 /usr/lib/libtelldus-core.so
7 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/routes/app/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route } from 'react-router-dom'
3 | import Home from '../home'
4 |
5 | const App = () => (
6 |
7 |
8 |
9 |
10 |
11 | )
12 |
13 | export default App
14 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/serverinfo.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type ServerInfo struct {
4 | Name string `json:"name"`
5 | UUID string `json:"uuid"`
6 | TLSPort string `json:"tlsPort"`
7 | Port string `json:"port"`
8 | Init bool `json:"init"`
9 | AllowLogin bool `json:"allowLogin"`
10 | }
11 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/nodes/stampzilla-nx-witness/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "time"
4 |
5 | type Config struct {
6 | Username string `json:"username"`
7 | Password string `json:"password"`
8 | Host string `json:"host"`
9 | Interval string `json:"interval"`
10 | interval time.Duration
11 | }
12 |
13 | func NewConfig() *Config {
14 | return &Config{}
15 | }
16 |
--------------------------------------------------------------------------------
/nodes/stampzilla-tibber/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "time"
4 |
5 | type Config struct {
6 | CarChargeDuration string `json:"carChargeDuration"`
7 | carChargeDuration time.Duration
8 | Token string `json:"token"`
9 | HomeID string `json:"homeId"`
10 | }
11 |
12 | func NewConfig() *Config {
13 | return &Config{}
14 | }
15 |
--------------------------------------------------------------------------------
/docs/screenshots.md:
--------------------------------------------------------------------------------
1 | # Screenshots of the web gui
2 |
3 | Dashboard
4 | 
5 |
6 | Automation
7 | 
8 |
9 | Nodes
10 | 
11 |
12 | Security
13 | 
14 |
15 | Debug
16 | 
17 |
--------------------------------------------------------------------------------
/nodes/stampzilla-google-assistant/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Config struct {
4 | Port string `json:"port"`
5 | ClientID string `json:"clientID"`
6 | ClientSecret string `json:"clientSecret"`
7 | ProjectID string `json:"projectID"`
8 | APIKey string `json:"APIKey"`
9 | }
10 |
11 | func NewConfig() *Config {
12 | return &Config{}
13 | }
14 |
--------------------------------------------------------------------------------
/nodes/stampzilla-modbus/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Config struct {
4 | Registers Registers
5 | Device string
6 | }
7 |
8 | func NewConfig() *Config {
9 | return &Config{
10 | Registers: make(Registers),
11 | }
12 | }
13 |
14 | type Register struct {
15 | Name string
16 | Id uint16
17 | Value interface{}
18 | Base int64
19 | }
20 |
21 | type Registers map[string]*Register
22 |
--------------------------------------------------------------------------------
/cmd/stampzilla/Makefile:
--------------------------------------------------------------------------------
1 | LDFLAGS=-ldflags "-X main.buildstamp \"`date -u '+%Y-%m-%d %H:%M:%S %Z'`\" -X main.githash `git rev-parse --short HEAD`"
2 |
3 | all:
4 | go build ${LDFLAGS} $(filter-out $@,$(MAKECMDGOALS))
5 |
6 | install:
7 | go install ${LDFLAGS} $(filter-out $@,$(MAKECMDGOALS))
8 |
9 | run:
10 | go build ${LDFLAGS}
11 | ./stampzilla $(filter-out $@,$(MAKECMDGOALS))
12 |
13 | %:
14 | @:
15 |
--------------------------------------------------------------------------------
/nodes/stampzilla-deconz/README.md:
--------------------------------------------------------------------------------
1 | # deconz
2 |
3 | Connects to a running deconz instance and can then control zigbee devices like IKEA trådfri and xiaomi sensors etc.
4 |
5 | ## Configuration
6 |
7 | It requires the password to the web interface and then automaticly creates a API user.
8 | ```
9 | {
10 | "ip": "192.168.13.1",
11 | "port": "9042",
12 | "password": "deconz password"
13 | }
14 | ```
15 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/message.go:
--------------------------------------------------------------------------------
1 | package notification
2 |
3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models"
4 |
5 | type Messages []Message
6 |
7 | type Message struct {
8 | DestinationSelector models.Labels `json:"destinationSelector"`
9 | Head string `json:"head"`
10 | Body string `json:"body"`
11 | }
12 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/node_test.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestSetAlias(t *testing.T) {
11 | n := &Node{}
12 | id, _ := devices.NewIDFromString("1.1")
13 | n.SetAlias(id, "alias")
14 | assert.Equal(t, "alias", n.Alias(id))
15 | }
16 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/connection.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "github.com/lesismal/melody"
4 |
5 | type Connection struct {
6 | Type string `json:"type"`
7 | RemoteAddr string `json:"remoteAddr"`
8 | NodeUuid string `json:"nodeUuid,omitEmpty"`
9 | Attributes map[string]interface{} `json:"attributes"`
10 |
11 | Session *melody.Session `json:"-"`
12 | }
13 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React + Redux App",
3 | "name": "Create React App Redux",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/ducks/index.js:
--------------------------------------------------------------------------------
1 | import forecast from './forecast'
2 | import devices from './devices'
3 | import config from './config'
4 | import { connectRouter } from 'connected-react-router/immutable'
5 | import { combineReducers } from 'redux-immutable'
6 |
7 | export default history =>
8 | combineReducers({
9 | forecast,
10 | devices,
11 | config,
12 | router: connectRouter(history)
13 | })
14 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Config struct {
4 | Weather struct {
5 | Current struct {
6 | Temperature struct {
7 | Device string `json:"device"`
8 | Field string `json:"field"`
9 | } `json:"temperature"`
10 | Humidity struct {
11 | Device string `json:"device"`
12 | Field string `json:"field"`
13 | } `json:"humidity"`
14 | } `json:"current"`
15 | } `json:"weather"`
16 | }
17 |
--------------------------------------------------------------------------------
/nodes/stampzilla-streamdeck/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "sync"
4 |
5 | type config struct {
6 | Pages map[string]page `json:"pages"`
7 | sync.Mutex
8 | }
9 |
10 | type page struct {
11 | Keys [15]key `json:"keys"`
12 | }
13 |
14 | type key struct {
15 | Name string `json:"name"`
16 | Node string `json:"node"`
17 | Device string `json:"device"`
18 | Action string `json:"action"`
19 | Field string `json:"field"`
20 | }
21 |
--------------------------------------------------------------------------------
/nodes/stampzilla-exoline/lab/celltohex/celltohex.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "strconv"
8 | )
9 |
10 | func main() {
11 | intVar, err := strconv.Atoi(os.Args[1])
12 | if err != nil {
13 | log.Panic(err)
14 | return
15 | }
16 |
17 | fmt.Println("celltoHex", cellToHex(intVar))
18 | }
19 |
20 | func cellToHex(c int) string {
21 | return fmt.Sprintf("%x", []byte{byte(c / 60), byte(c % 60)})
22 | }
23 |
--------------------------------------------------------------------------------
/nodes/stampzilla-exoline/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Config struct {
4 | Interval string
5 | Host string
6 | Variables []Variables
7 | }
8 |
9 | type Variables struct {
10 | Name string `json:"name"`
11 | LoadNumber int `json:"loadNumber"`
12 | Cell int `json:"cell"`
13 | Type string `json:"type"`
14 | Write bool `json:"write"`
15 | }
16 |
17 | func NewConfig() *Config {
18 | return &Config{}
19 | }
20 |
--------------------------------------------------------------------------------
/nodes/stampzilla-exoline/lab/hextocell/hextocell.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 | "log"
7 | "os"
8 | )
9 |
10 | func main() {
11 | fmt.Println("hexToCell", hexToCell(os.Args[1]))
12 | }
13 | func hexToCell(s string) int {
14 | v, err := hex.DecodeString(s)
15 | if err != nil {
16 | log.Println(err)
17 | return 0
18 | }
19 | first := int(v[0])
20 | second := int(v[1])
21 | return first*60 + second
22 | }
23 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/middlewares/toast.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { toast } from 'react-toastify';
3 |
4 | const toastify = (store) => (next) => async (action) => {
5 | try {
6 | return await next(action);
7 | } catch (ex) {
8 | toast.error(
9 | <>
10 | Save failed:
11 | {' '}
12 | {ex}
13 | >,
14 | );
15 |
16 | throw ex;
17 | }
18 | };
19 |
20 | export default toastify;
21 |
--------------------------------------------------------------------------------
/nodes/stampzilla-exoline/exoline/exoline_test.go:
--------------------------------------------------------------------------------
1 | package exoline
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestFloat64bytes(t *testing.T) {
10 | f := 12.34
11 | b := FloatTobytes(f)
12 | assert.Equal(t, []byte{164, 112, 69, 65}, b)
13 | }
14 |
15 | func TestAsRoundedFloat(t *testing.T) {
16 | b := []byte{164, 112, 69, 65}
17 | f, err := AsRoundedFloat(b)
18 | assert.NoError(t, err)
19 | assert.Equal(t, 12.34, f)
20 | }
21 |
--------------------------------------------------------------------------------
/nodes/stampzilla-tibber/tibber_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | /*
4 | func TestWS(t *testing.T) {
5 |
6 | u, err := getWsURL("token", "homeid")
7 | assert.NoError(t, err)
8 | fmt.Println("url is", u)
9 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
10 | defer cancel()
11 | err = reconnectWS(ctx, u, "token", "homeid", func(data *DataPayload) {
12 | fmt.Println("the data is")
13 | spew.Dump(data)
14 | })
15 | assert.NoError(t, err)
16 | }
17 | */
18 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/interfaces/melody.go:
--------------------------------------------------------------------------------
1 | package interfaces
2 |
3 | type MelodyWriter interface {
4 | Write(msg []byte) error
5 | }
6 |
7 | type MelodySession interface {
8 | MelodyWriter
9 | Close() error
10 | CloseWithMsg(msg []byte) error
11 | Get(key string) (value interface{}, exists bool)
12 | IsClosed() bool
13 | // MustGet(key string) interface{} // removed in github.com/lesismal/melody fork
14 | Set(key string, value interface{})
15 | WriteBinary(msg []byte) error
16 | }
17 |
--------------------------------------------------------------------------------
/nodes/stampzilla-modbus/decode_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestDecode(t *testing.T) {
10 | data1 := []byte{0xff, 0xff} // -0.1
11 | data2 := []byte{0xff, 0xfa} // -0.6
12 | data3 := []byte{0x00, 0x3c} // 6
13 | data4 := []byte{0x00, 0x02} // 0.2
14 |
15 | assert.Equal(t, -0.1, decode(data1)/10)
16 | assert.Equal(t, -0.6, decode(data2)/10)
17 | assert.Equal(t, 6.0, decode(data3)/10)
18 | assert.Equal(t, 0.2, decode(data4)/10)
19 | }
20 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/README.md:
--------------------------------------------------------------------------------
1 | # Server
2 |
3 | The server is the main component. It has the following responsibilities among other things.
4 |
5 | * Stores the config for all the nodes and sent it to them when they start.
6 | * Serves a web gui.
7 | * Stores and evalutate rules.
8 | * Stores and runs schedules.
9 |
10 |
11 | ### Developing
12 |
13 | Install deps
14 |
15 | ```
16 | dep ensure -vendor-only
17 |
18 | ```
19 |
20 | Allow https to localhost in chrome
21 | ```
22 | chrome://flags/#allow-insecure-localhost
23 | ```
24 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/middlewares/rules.js:
--------------------------------------------------------------------------------
1 | import { write } from '../components/Websocket';
2 |
3 | const rules = store => next => (action) => {
4 | const prev = store.getState().getIn(['rules', 'list']);
5 | const result = next(action);
6 | const after = store.getState().getIn(['rules', 'list']);
7 |
8 | if (!after.equals(prev) && action.type !== 'rules_UPDATE') {
9 | write({
10 | type: 'update-rules',
11 | body: after.toJS(),
12 | });
13 | }
14 | return result;
15 | };
16 |
17 | export default rules;
18 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/routes/automation/components/Scene.scss:
--------------------------------------------------------------------------------
1 | .saved-state-builder {
2 | .menu {
3 | display: flex;
4 | background: #eee;
5 | padding: 6px 14px;
6 | margin-left: -16px;
7 | margin-right: -16px;
8 | }
9 |
10 | .devices {
11 | display: flex;
12 | flex-direction: column;
13 |
14 | color: #999;
15 |
16 | .trait {
17 | filter: blur(2px);
18 | }
19 | .selected {
20 | color: #000;
21 |
22 | .trait {
23 | filter: none;
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/senders.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/notification"
4 |
5 | func (store *Store) GetSenders() map[string]notification.Sender {
6 | store.RLock()
7 | defer store.RUnlock()
8 | return store.Senders.All()
9 | }
10 |
11 | func (store *Store) AddOrUpdateSender(sender notification.Sender) {
12 | if sender.UUID == "" {
13 | return
14 | }
15 |
16 | store.Senders.Add(sender)
17 | store.Senders.Save("senders.json")
18 | store.runCallbacks("senders")
19 | }
20 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/middlewares/senders.js:
--------------------------------------------------------------------------------
1 | import { write } from '../components/Websocket';
2 |
3 | const senders = (store) => (next) => (action) => {
4 | const prev = store.getState().getIn(['senders', 'list']);
5 | const result = next(action);
6 | const after = store.getState().getIn(['senders', 'list']);
7 |
8 | if (!after.equals(prev) && action.type !== 'senders_UPDATE') {
9 | write({
10 | type: 'update-senders',
11 | body: after.toJS(),
12 | });
13 | }
14 | return result;
15 | };
16 |
17 | export default senders;
18 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/handlers/websockethandler.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 |
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/interfaces"
8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models"
9 | )
10 |
11 | type WebsocketHandler interface {
12 | Message(s interfaces.MelodySession, msg *models.Message) (json.RawMessage, error)
13 | Connect(s interfaces.MelodySession, r *http.Request, keys map[string]interface{}) error
14 | Disconnect(s interfaces.MelodySession) error
15 | }
16 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/middlewares/schedules.js:
--------------------------------------------------------------------------------
1 | import { write } from '../components/Websocket';
2 |
3 | const schedules = store => next => (action) => {
4 | const prev = store.getState().getIn(['schedules', 'list']);
5 | const result = next(action);
6 | const after = store.getState().getIn(['schedules', 'list']);
7 |
8 | if (!after.equals(prev) && action.type !== 'schedules_UPDATE') {
9 | write({
10 | type: 'update-schedules',
11 | body: after.toJS(),
12 | });
13 | }
14 | return result;
15 | };
16 |
17 | export default schedules;
18 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/middlewares/savedstates.js:
--------------------------------------------------------------------------------
1 | import { write } from '../components/Websocket';
2 |
3 | const savedstates = store => next => (action) => {
4 | const prev = store.getState().getIn(['savedstates', 'list']);
5 | const result = next(action);
6 | const after = store.getState().getIn(['savedstates', 'list']);
7 |
8 | if (!after.equals(prev) && action.type !== 'savedstates_UPDATE') {
9 | write({
10 | type: 'update-savedstates',
11 | body: after.toJS(),
12 | });
13 | }
14 | return result;
15 | };
16 |
17 | export default savedstates;
18 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/middlewares/destinations.js:
--------------------------------------------------------------------------------
1 | import { write } from '../components/Websocket';
2 |
3 | const senders = (store) => (next) => (action) => {
4 | const prev = store.getState().getIn(['destinations', 'list']);
5 | const result = next(action);
6 | const after = store.getState().getIn(['destinations', 'list']);
7 |
8 | if (!after.equals(prev) && action.type !== 'destinations_UPDATE') {
9 | write({
10 | type: 'update-destinations',
11 | body: after.toJS(),
12 | });
13 | }
14 | return result;
15 | };
16 |
17 | export default senders;
18 |
--------------------------------------------------------------------------------
/pkg/types/duration.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 | )
8 |
9 | type Duration time.Duration
10 |
11 | func (d Duration) String() string {
12 | return time.Duration(d).String()
13 | }
14 |
15 | func (d Duration) MarshalJSON() (b []byte, err error) {
16 | return []byte(fmt.Sprintf(`"%s"`, time.Duration(d).String())), nil
17 | }
18 |
19 | func (d *Duration) UnmarshalJSON(b []byte) error {
20 | td, err := time.ParseDuration(strings.Trim(string(b), `"`))
21 | if err != nil {
22 | return err
23 | }
24 | *d = Duration(td)
25 | return nil
26 | }
27 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/images/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stampzilla-go",
3 | "short_name": "stampzilla",
4 | "icons": [
5 | {
6 | "src": "/images/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/images/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/nodes/stampzilla-google-assistant/README.md:
--------------------------------------------------------------------------------
1 | # google-assistant
2 |
3 | Exposes all the devices in the server for control in google assistant (google home etc)
4 |
5 | ## Configuration
6 |
7 | The port is what port to listen to for google actions API calls. That port must be avilable from the internet.
8 | You need to create a project in google actions console: https://developers.google.com/actions/smarthome/create#create-project
9 | And fill in the values in the config below.
10 |
11 | ```
12 | {
13 | "port": "8000",
14 | "clientID": "",
15 | "clientSecret": "",
16 | "projectID": "",
17 | "APIKey": ""
18 | }
19 | ```
20 |
--------------------------------------------------------------------------------
/pkg/installer/prepare.go:
--------------------------------------------------------------------------------
1 | package installer
2 |
3 | func Prepare() error {
4 | // Make sure our infrastructure is correct
5 | //// Create required user and folders
6 | CreateUser("stampzilla")
7 | CreateDirAsUser("/var/spool/stampzilla", "stampzilla")
8 | CreateDirAsUser("/var/log/stampzilla", "stampzilla")
9 | CreateDirAsUser("/home/stampzilla", "stampzilla")
10 | CreateDirAsUser("/home/stampzilla/go", "stampzilla")
11 | CreateDirAsUser("/home/stampzilla/go/bin", "stampzilla")
12 | CreateDirAsUser("/etc/stampzilla", "stampzilla")
13 | CreateDirAsUser("/etc/stampzilla/nodes", "stampzilla")
14 |
15 | c := Config{}
16 | c.CreateConfig()
17 |
18 | return nil
19 | }
20 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/app.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction(
5 | 'app',
6 | ['UPDATE'],
7 | );
8 |
9 | const defaultState = Map({
10 | url: `${window.location.protocol.match(/^https/) ? 'wss' : 'ws'}://${window.location.host}/ws`,
11 | });
12 |
13 | export const update = state => (
14 | { type: c.UPDATE, state }
15 | );
16 |
17 | export default function reducer(state = defaultState, action) {
18 | switch (action.type) {
19 | case c.UPDATE: {
20 | return state
21 | .mergeDeep(action.state);
22 | }
23 | default: {
24 | return state;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/example-config.json:
--------------------------------------------------------------------------------
1 | {
2 | widgets: [
3 | {
4 | type: 'clock'
5 | },
6 | {
7 | type: 'devicelist',
8 | devices: [
9 | {
10 | title: 'Temperatur',
11 | device: 'dfc28c72-0e5c-451b-add2-c99af57510c8.1',
12 | state: 'on',
13 | states: {
14 | true: 'Till',
15 | false: 'Från'
16 | }
17 | },
18 | {
19 | title: 'Luftfuktighet',
20 | device: 'dfc28c72-0e5c-451b-add2-c99af57510c8.2',
21 | state: 'on',
22 | unit: 'grader'
23 | }
24 | ]
25 | },
26 | {
27 | type: 'forecast'
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/installer/installer.go:
--------------------------------------------------------------------------------
1 | package installer
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/stampzilla/stampzilla-go/v2/pkg/installer/binary"
7 | "github.com/stampzilla/stampzilla-go/v2/pkg/installer/source"
8 | )
9 |
10 | type Installer interface {
11 | Prepare() error
12 | Install(...string) error
13 | Update(...string) error
14 | }
15 |
16 | type InstallSource uint8
17 |
18 | const (
19 | Binaries = iota
20 | SourceCode
21 | )
22 |
23 | func New(s InstallSource) (Installer, error) {
24 | switch s {
25 | case Binaries:
26 | return binary.NewInstaller(), nil
27 | case SourceCode:
28 | return source.NewInstaller(), nil
29 | }
30 | return nil, fmt.Errorf("No installer with value %d is available", s)
31 | }
32 |
--------------------------------------------------------------------------------
/nodes/stampzilla-nibe/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/sirupsen/logrus"
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-nibe/nibe"
8 | )
9 |
10 | type Config struct {
11 | Port string `json:"port"`
12 | }
13 |
14 | func updatedConfig(n *nibe.Nibe) func(data json.RawMessage) error {
15 | return func(data json.RawMessage) error {
16 | logrus.Info("Received config from server:", string(data))
17 |
18 | newConf := &Config{}
19 | err := json.Unmarshal(data, newConf)
20 | if err != nil {
21 | return err
22 | }
23 |
24 | config = newConf
25 | logrus.Info("Config is now: ", config)
26 |
27 | n.Connect(config.Port)
28 |
29 | return nil
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/middlewares/persons.js:
--------------------------------------------------------------------------------
1 | import { request } from '../components/Websocket';
2 | import reducer from '../ducks/persons';
3 |
4 | const rules = (store) => (next) => async (action) => {
5 | if (action.type.startsWith('persons_') && !action.type.endsWith('_UPDATE')) {
6 | // Generate a updated state by running the reducer
7 | const updatedState = reducer(store.getState().get('persons'), action);
8 |
9 | // Try to save
10 | await request({
11 | type: 'update-persons',
12 | body: updatedState.get('list').toJS(),
13 | });
14 |
15 | // Jump to the next middleware
16 | return next(action);
17 | }
18 |
19 | return next(action);
20 | };
21 |
22 | export default rules;
23 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/routes/dashboard/HueColorPicker.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { CustomPicker } from 'react-color';
3 | import { Hue } from 'react-color/lib/components/common';
4 | import SliderPointer from 'react-color/lib/components/slider/SliderPointer';
5 |
6 | class HueColorPicker extends PureComponent {
7 | render() {
8 | return (
9 |
10 |
16 |
17 | );
18 | }
19 | }
20 |
21 | export default CustomPicker(HueColorPicker);
22 |
--------------------------------------------------------------------------------
/pkg/build/build.go:
--------------------------------------------------------------------------------
1 | package build
2 |
3 | import "fmt"
4 |
5 | // Version is used by CI/CD system to set the version of the built binary.
6 | var Version = "dev"
7 |
8 | // BuildTime is the time of this build.
9 | var BuildTime = ""
10 |
11 | // Commit contains the SHA commit hash.
12 | var Commit = ""
13 |
14 | // String returns the version info as a string.
15 | func String() string {
16 | return fmt.Sprintf(`Version: "%s", BuildTime: "%s", Commit: "%s" `, Version, BuildTime, Commit)
17 | }
18 |
19 | func JSON() string {
20 | return fmt.Sprintf(`{"Version": "%s", "BuildTime": "%s", "Commit": "%s"} `, Version, BuildTime, Commit)
21 | }
22 |
23 | type Data struct {
24 | Version string
25 | BuildTime string
26 | Commit string
27 | }
28 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import { ConnectedRouter } from 'connected-react-router/immutable'
5 | import store, { history } from './store'
6 | import App from './routes/app'
7 | import Websocket from './components/Websocket'
8 |
9 | import 'sanitize.css/sanitize.css'
10 | import './index.css'
11 |
12 | const target = document.querySelector('#root')
13 |
14 | render(
15 |
16 |
17 |
18 |
21 |
22 | ,
23 | target
24 | )
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.crt
2 | *.key
3 |
4 | #Compiled Object files, Static and Dynamic libs (Shared Objects)
5 | *.o
6 | *.a
7 | *.so
8 |
9 | # Folders
10 | _obj
11 | _test
12 |
13 | # Architecture specific extensions/prefixes
14 | *.[568vq]
15 | [568vq].out
16 |
17 | *.cgo1.go
18 | *.cgo2.c
19 | _cgo_defun.c
20 | _cgo_gotypes.go
21 | _cgo_export.*
22 |
23 | _testmain.go
24 |
25 | *.exe
26 |
27 | nodes/stampzilla-server/public/bower_components/
28 | stampzilla-server/gin-bin
29 |
30 | # application specifics
31 | config.json
32 | devices.json
33 |
34 | dist
35 |
36 | #vim stuff
37 | # swap
38 | [._]*.s[a-w][a-z]
39 | [._]s[a-w][a-z]
40 | # session
41 | Session.vim
42 | # temporary
43 | .netrwhist
44 | *~
45 | # auto-generated tag files
46 | tags
47 | coverage.txt
48 |
--------------------------------------------------------------------------------
/nodes/stampzilla-mbus/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Config struct {
4 | Interval string `json:"interval"`
5 | Host string `json:"host"`
6 | Port string `json:"port"`
7 | Devices []Device `json:"devices"`
8 | }
9 |
10 | type Device struct {
11 | Interval string `json:"interval"`
12 | Enabled bool `json:"enabled"`
13 | Name string `json:"name"`
14 | PrimaryAddress int `json:"primaryAddress"`
15 | // Frames which frames and datarecord to fetch.
16 | // Only 0 (first frame) is supported for now
17 | Frames map[string][]Record `json:"frames"`
18 | }
19 |
20 | type Record struct {
21 | Id int `json:"id"`
22 | Name string `json:"name"`
23 | }
24 |
25 | func NewConfig() *Config {
26 | return &Config{}
27 | }
28 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/ducks/config.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable'
2 | import { defineAction } from 'redux-define'
3 |
4 | const c = defineAction('config', ['UPDATE'])
5 |
6 | const defaultState = Map({})
7 |
8 | // Actions
9 | export function update(config) {
10 | return { type: c.UPDATE, config }
11 | }
12 |
13 | // Subscribe to channels and register the action for the packages
14 | export function subscribe(dispatch) {
15 | return {
16 | config: config => dispatch(update(config))
17 | }
18 | }
19 |
20 | // Reducer
21 | export default function reducer(state = defaultState, action) {
22 | switch (action.type) {
23 | case c.UPDATE: {
24 | return fromJS(action.config)
25 | }
26 | default:
27 | return state
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false
7 | }
8 | ],
9 | "@babel/preset-react",
10 | ],
11 | "plugins": [
12 | "react-hot-loader/babel",
13 | "@babel/plugin-syntax-dynamic-import",
14 | "@babel/plugin-syntax-import-meta",
15 | "@babel/plugin-proposal-class-properties",
16 | "@babel/plugin-proposal-json-strings",
17 | [
18 | "@babel/plugin-proposal-decorators",
19 | {
20 | "legacy": true
21 | }
22 | ],
23 | "@babel/plugin-proposal-function-sent",
24 | "@babel/plugin-proposal-export-namespace-from",
25 | "@babel/plugin-proposal-numeric-separator",
26 | "@babel/plugin-proposal-throw-expressions"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/server.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction('server', ['UPDATE']);
5 |
6 | const defaultState = Map({});
7 |
8 | export const update = state => (dispatch, getState) => {
9 | if (
10 | !getState()
11 | .get('server')
12 | .equals(
13 | getState()
14 | .get('server')
15 | .mergeDeep(state),
16 | )
17 | ) {
18 | dispatch({ type: c.UPDATE, state });
19 | }
20 | };
21 |
22 | export default function reducer(state = defaultState, action) {
23 | switch (action.type) {
24 | case c.UPDATE: {
25 | return state.mergeDeep(action.state);
26 | }
27 | default: {
28 | return state;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/types/duration_test.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestMarshalJSOn(t *testing.T) {
12 | s := struct {
13 | D Duration
14 | }{
15 | D: Duration(time.Second),
16 | }
17 |
18 | j, err := json.Marshal(&s)
19 | assert.NoError(t, err)
20 | assert.Equal(t, `{"D":"1s"}`, string(j))
21 | }
22 |
23 | func TestUnmarshalJSON(t *testing.T) {
24 | s := struct {
25 | D Duration
26 | }{}
27 |
28 | err := json.Unmarshal([]byte(`{"D":"1s"}`), &s)
29 | assert.NoError(t, err)
30 | assert.Equal(t, Duration(time.Second), s.D)
31 |
32 | err = json.Unmarshal([]byte(`{"D":"200ms"}`), &s)
33 | assert.NoError(t, err)
34 | assert.Equal(t, Duration(time.Millisecond*200), s.D)
35 | }
36 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/devices.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
4 |
5 | func (store *Store) GetDevices() *devices.List {
6 | store.RLock()
7 | defer store.RUnlock()
8 | return store.Devices
9 | }
10 |
11 | func (store *Store) AddOrUpdateDevice(dev *devices.Device) {
12 | if dev == nil {
13 | return
14 | }
15 |
16 | oldDev := store.Devices.Get(dev.ID)
17 | if oldDev != nil && oldDev.Equal(dev) {
18 | return
19 | }
20 |
21 | store.Devices.Add(dev)
22 | node := store.GetNode(dev.ID.Node)
23 |
24 | alias := node.Alias(dev.ID)
25 | if alias != dev.Alias {
26 | dev.Lock()
27 | dev.Alias = alias
28 | dev.Unlock()
29 | }
30 |
31 | store.Logic.UpdateDevice(dev)
32 | store.runCallbacks("devices")
33 | }
34 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/main.go:
--------------------------------------------------------------------------------
1 | //go:generate bash -c "go get -u github.com/rakyll/statik && cd web && rm -rf dist && npm run build && cd .. && statik -src ./web/dist -f"
2 | package main
3 |
4 | import (
5 | "fmt"
6 |
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models"
8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/servermain"
9 |
10 | // Statik for the webserver gui.
11 | _ "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/statik"
12 | "github.com/stampzilla/stampzilla-go/v2/pkg/build"
13 | )
14 |
15 | func main() {
16 | config := &models.Config{}
17 | config.MustLoad()
18 |
19 | if config.Version {
20 | fmt.Println(build.String())
21 | return
22 | }
23 |
24 | server := servermain.New(config)
25 | server.Init()
26 | server.Run()
27 | }
28 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | sudo: false
3 | addons:
4 | apt:
5 | packages:
6 | - libasound2-dev
7 |
8 | go:
9 | - 1.20
10 | install:
11 | - go get -d -t -v ./...
12 |
13 | script:
14 | - make test
15 | - make cover
16 | after_success:
17 | - bash <(curl -s https://codecov.io/bash)
18 |
19 | before_deploy:
20 | - go run cmd/build/build.go
21 | - cd dist && sha512sum * > checksum
22 |
23 | deploy:
24 | skip_cleanup: true
25 | provider: releases
26 | api_key:
27 | secure: bbn0U42cMuDYAQGCMaPXbkTOygk3Sr/P8u998Y13RVFEe/rcM4UQ9WjvhOMFlMLS3gN6Gqu/Qm3cT5z4AawY0JPf1PnbGprTMCZEzrHPmd203cAMRmcpzTYBEkioRRTgOS5syVxxY8ZNKCW4+/QAmngai3uU/CL/4aPL5T6SQSk=
28 | file_glob: true
29 | file: "*"
30 | on:
31 | tags: true
32 | repo: stampzilla/stampzilla-go
33 | condition: $TRAVIS_GO_VERSION =~ ^1\.20
34 |
--------------------------------------------------------------------------------
/nodes/stampzilla-linux/health.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 |
6 | sigar "github.com/cloudfoundry/gosigar"
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
8 | )
9 |
10 | func monitorHealth() {
11 | dev := &devices.Device{
12 | Name: "Health",
13 | ID: devices.ID{ID: "health"},
14 | Online: true,
15 | State: devices.State{},
16 | }
17 | n.AddOrUpdate(dev)
18 |
19 | for {
20 | uptime := sigar.Uptime{}
21 | uptime.Get()
22 | avg := sigar.LoadAverage{}
23 | avg.Get()
24 |
25 | newState := make(devices.State)
26 | newState["uptime"] = uptime.Format()
27 | newState["load_1"] = avg.One
28 | newState["load_5"] = avg.Five
29 | newState["load_15"] = avg.Fifteen
30 | n.UpdateState(dev.ID.ID, newState)
31 |
32 | <-time.After(time.Second * 1)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/docs/devices.md:
--------------------------------------------------------------------------------
1 | # Devices
2 |
3 | A device is a single unit which is controllable and/or reports some data. For example sensor, light etc.
4 |
5 | ### Device types
6 |
7 | type | Description
8 | --- | ---
9 | light | Can switch on or off
10 | sensor | can report any sensor data for example temperature
11 | button | Is a momentary button which can be pressed
12 |
13 |
14 | ### Traits
15 |
16 | A device can have one or multiple traits. This was inspired by the google actions API.
17 |
18 | ##### OnOff
19 |
20 | Required states
21 |
22 | state | type
23 | --- | ---
24 | on | bool
25 |
26 | ##### Brightness
27 |
28 | Required states
29 |
30 | state | type
31 | --- | ---
32 | brightness | float 0-1
33 |
34 | ##### ColorSetting
35 |
36 | Required states
37 |
38 | state | type
39 | --- | ---
40 | temperature | int 2000-6500 kelvin
41 |
42 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/nodes.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction(
5 | 'nodes',
6 | ['UPDATE'],
7 | );
8 |
9 | const defaultState = Map({
10 | list: Map(),
11 | });
12 |
13 | // Actions
14 | export function update(connections) {
15 | return { type: c.UPDATE, connections };
16 | }
17 |
18 | // Subscribe to channels and register the action for the packages
19 | export function subscribe(dispatch) {
20 | return {
21 | nodes: nodes => dispatch(update(nodes)),
22 | };
23 | }
24 |
25 | // Reducer
26 | export default function reducer(state = defaultState, action) {
27 | switch (action.type) {
28 | case c.UPDATE: {
29 | return state
30 | .set('list', fromJS(action.connections));
31 | }
32 | default: return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/devices.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction(
5 | 'devices',
6 | ['UPDATE'],
7 | );
8 |
9 | const defaultState = Map({
10 | list: Map(),
11 | });
12 |
13 | // Actions
14 | export function update(connections) {
15 | return { type: c.UPDATE, connections };
16 | }
17 |
18 | // Subscribe to channels and register the action for the packages
19 | export function subscribe(dispatch) {
20 | return {
21 | devices: devices => dispatch(update(devices)),
22 | };
23 | }
24 |
25 | // Reducer
26 | export default function reducer(state = defaultState, action) {
27 | switch (action.type) {
28 | case c.UPDATE: {
29 | return state
30 | .set('list', fromJS(action.connections));
31 | }
32 | default: return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/ducks/devices.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction(
5 | 'devices',
6 | ['UPDATE'],
7 | );
8 |
9 | const defaultState = Map({
10 | list: Map(),
11 | });
12 |
13 | // Actions
14 | export function update(connections) {
15 | return { type: c.UPDATE, connections };
16 | }
17 |
18 | // Subscribe to channels and register the action for the packages
19 | export function subscribe(dispatch) {
20 | return {
21 | devices: devices => dispatch(update(devices)),
22 | };
23 | }
24 |
25 | // Reducer
26 | export default function reducer(state = defaultState, action) {
27 | switch (action.type) {
28 | case c.UPDATE: {
29 | return state
30 | .set('list', fromJS(action.connections));
31 | }
32 | default: return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/requests.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction(
5 | 'requests',
6 | ['UPDATE'],
7 | );
8 |
9 | const defaultState = Map({
10 | list: Map(),
11 | });
12 |
13 | // Actions
14 | export function update(connections) {
15 | return { type: c.UPDATE, connections };
16 | }
17 |
18 | // Subscribe to channels and register the action for the packages
19 | export function subscribe(dispatch) {
20 | return {
21 | requests: requests => dispatch(update(requests)),
22 | };
23 | }
24 |
25 | // Reducer
26 | export default function reducer(state = defaultState, action) {
27 | switch (action.type) {
28 | case c.UPDATE: {
29 | return state
30 | .set('list', fromJS(action.connections));
31 | }
32 | default: return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/connections.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction(
5 | 'connections',
6 | ['UPDATE'],
7 | );
8 |
9 | const defaultState = Map({
10 | list: Map(),
11 | });
12 |
13 | // Actions
14 | export function update(connections) {
15 | return { type: c.UPDATE, connections };
16 | }
17 |
18 | // Subscribe to channels and register the action for the packages
19 | export function subscribe(dispatch) {
20 | return {
21 | connections: connections => dispatch(update(connections)),
22 | };
23 | }
24 |
25 | // Reducer
26 | export default function reducer(state = defaultState, action) {
27 | switch (action.type) {
28 | case c.UPDATE: {
29 | return state
30 | .set('list', fromJS(action.connections));
31 | }
32 | default: return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/certificates.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction(
5 | 'certificates',
6 | ['UPDATE'],
7 | );
8 |
9 | const defaultState = Map({
10 | list: Map(),
11 | });
12 |
13 | // Actions
14 | export function update(connections) {
15 | return { type: c.UPDATE, connections };
16 | }
17 |
18 | // Subscribe to channels and register the action for the packages
19 | export function subscribe(dispatch) {
20 | return {
21 | certificates: certificates => dispatch(update(certificates)),
22 | };
23 | }
24 |
25 | // Reducer
26 | export default function reducer(state = defaultState, action) {
27 | switch (action.type) {
28 | case c.UPDATE: {
29 | return state
30 | .set('list', fromJS(action.connections));
31 | }
32 | default: return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/certificates.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import "time"
4 |
5 | type Certificate struct {
6 | Serial string `json:"serial"`
7 | Subject RequestSubject `json:"subject"`
8 | CommonName string `json:"commonName"`
9 | IsCA bool `json:"isCA"`
10 | Usage []string `json:"usage"`
11 | Revoked bool `json:"revoked"`
12 | Issued time.Time `json:"issued"`
13 | Expires time.Time `json:"expires"`
14 |
15 | Fingerprints map[string]string `json:"fingerprints"`
16 | }
17 |
18 | func (store *Store) GetCertificates() []Certificate {
19 | store.RLock()
20 | defer store.RUnlock()
21 | return store.Certificates
22 | }
23 |
24 | func (store *Store) UpdateCertificates(certs []Certificate) {
25 | store.Lock()
26 | store.Certificates = certs
27 | store.Unlock()
28 |
29 | store.runCallbacks("certificates")
30 | }
31 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 |
4 | "plugins": [
5 | "react",
6 | "html"
7 | ],
8 |
9 | "env": {
10 | "browser": true
11 | },
12 |
13 | "extends": "airbnb",
14 |
15 | "rules": {
16 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
17 | "react/require-default-props": 0,
18 | "jsx-a11y/media-has-caption": 0,
19 | "function-paren-newline": ["error", "consistent"],
20 | "jsx-a11y/anchor-is-valid": 0,
21 | "jsx-a11y/click-events-have-key-events": 0,
22 | "object-curly-newline": ["error", { "minProperties": 4, "consistent": true }],
23 | "jsx-a11y/label-has-for": 0,
24 | "jsx-a11y/no-autofocus": 0,
25 | "camelcase": 0,
26 | },
27 |
28 | "settings": {
29 | "import/resolver": {
30 | "node": {
31 | "paths": ["src"]
32 | }
33 | },
34 | "html/report-bad-indent": "warn"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | * Tutorial: [Getting started with create-react-app, Redux, React Router & Redux Thunk](https://medium.com/@notrab/getting-started-with-create-react-app-redux-react-router-redux-thunk-d6a19259f71f)
4 | * [Demo](https://create-react-app-redux.now.sh) 🙌
5 |
6 | ## Installation
7 |
8 | ```bash
9 | git clone https://github.com/notrab/create-react-app-redux.git
10 | cd create-react-app-redux
11 | yarn
12 | ```
13 |
14 | ## Get started
15 |
16 | ```bash
17 | yarn start
18 | ```
19 |
20 | This boilerplate is built using [create-react-app](https://github.com/facebook/create-react-app) so you will want to read the User Guide for more goodies.
21 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux-immutable';
2 |
3 | import app from './app';
4 | import certificates from './certificates';
5 | import connection from './connection';
6 | import connections from './connections';
7 | import destinations from './destinations';
8 | import devices from './devices';
9 | import nodes from './nodes';
10 | import persons from './persons';
11 | import requests from './requests';
12 | import rules from './rules';
13 | import savedstates from './savedstates';
14 | import schedules from './schedules';
15 | import senders from './senders';
16 | import server from './server';
17 |
18 | const rootReducer = combineReducers({
19 | app,
20 | certificates,
21 | connection,
22 | connections,
23 | destinations,
24 | devices,
25 | nodes,
26 | persons,
27 | requests,
28 | rules,
29 | savedstates,
30 | schedules,
31 | senders,
32 | server,
33 | });
34 |
35 | export default rootReducer;
36 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/CustomCheckbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const CustomCheckbox = (props) => {
4 | const {
5 | id,
6 | value,
7 | required,
8 | disabled,
9 | readonly,
10 | label,
11 | autofocus,
12 | onChange,
13 | } = props;
14 | return (
15 |
16 | onChange(event.target.checked)}
25 | />
26 |
29 |
30 | );
31 | };
32 |
33 | export default CustomCheckbox;
34 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import { routerMiddleware } from 'connected-react-router/immutable'
3 | import thunk from 'redux-thunk'
4 | import createHistory from 'history/createBrowserHistory'
5 | import rootReducer from './ducks'
6 | import { Map } from 'immutable'
7 |
8 | export const history = createHistory()
9 |
10 | const initialState = Map({})
11 | const enhancers = []
12 | const middleware = [thunk, routerMiddleware(history)]
13 |
14 | if (process.env.NODE_ENV === 'development') {
15 | const devToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION__
16 |
17 | if (typeof devToolsExtension === 'function') {
18 | enhancers.push(devToolsExtension())
19 | }
20 | }
21 |
22 | const composedEnhancers = compose(
23 | applyMiddleware(...middleware),
24 | ...enhancers
25 | )
26 |
27 | export default createStore(
28 | rootReducer(history),
29 | initialState,
30 | composedEnhancers
31 | )
32 |
--------------------------------------------------------------------------------
/nodes/stampzilla-modbus/modbus.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/goburrow/modbus"
7 | )
8 |
9 | type Modbus struct {
10 | client modbus.Client
11 | handler *modbus.RTUClientHandler
12 | }
13 |
14 | func (m *Modbus) Connect() error {
15 | // Modbus RTU/ASCII
16 | handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
17 | handler.BaudRate = 9600
18 | handler.DataBits = 8
19 | handler.Parity = "N"
20 | handler.StopBits = 2
21 | handler.SlaveId = 1
22 | handler.Timeout = 5 * time.Second
23 | // handler.Logger = log.New(os.Stdout, "test: ", log.LstdFlags)
24 | m.handler = handler
25 | if err := handler.Connect(); err != nil {
26 | return err
27 | }
28 |
29 | m.client = modbus.NewClient(handler)
30 | return nil
31 | }
32 |
33 | func (m *Modbus) Close() error {
34 | return m.handler.Close()
35 | }
36 |
37 | func (m *Modbus) ReadInputRegister(address uint16) ([]byte, error) {
38 | return m.client.ReadInputRegisters(address-1, 1)
39 | }
40 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/server.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/sirupsen/logrus"
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
8 | )
9 |
10 | func (s *Store) AddOrUpdateServer(area, item string, state devices.State) {
11 | s.Lock()
12 | if s.Server[area] == nil {
13 | s.Server[area] = make(map[string]devices.State)
14 | }
15 | if s.Server[area][item] == nil {
16 | s.Server[area][item] = make(devices.State)
17 | }
18 |
19 | if diff := s.Server[area][item].Diff(state); len(diff) != 0 {
20 | s.Server[area][item].MergeWith(diff)
21 | s.Unlock()
22 | s.runCallbacks("server")
23 | return
24 | }
25 | s.Unlock()
26 | }
27 |
28 | func (store *Store) GetServerStateAsJson() json.RawMessage {
29 | store.RLock()
30 | b, err := json.Marshal(store.Server)
31 | store.RUnlock()
32 |
33 | if err != nil {
34 | logrus.Errorf("Failed to marshal server state: %s", err.Error())
35 | }
36 | return b
37 | }
38 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/SocketModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import FormModal from './FormModal';
4 |
5 | const schema = {
6 | type: 'object',
7 | required: ['hostname', 'port'],
8 | properties: {
9 | hostname: { type: 'string', title: 'Hostname', default: location.hostname },
10 | port: { type: 'string', title: 'Port', default: location.port },
11 | },
12 | };
13 |
14 | class SocketModal extends Component {
15 | onChange = () => ({ formData }) => {
16 | this.props.onChange(formData);
17 | this.props.onClose();
18 | };
19 |
20 | render = () => {
21 | const { visible, onClose } = this.props;
22 |
23 | return (
24 |
32 | );
33 | };
34 | }
35 |
36 | export default SocketModal;
37 |
--------------------------------------------------------------------------------
/coverage:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | readonly GOPATH="${GOPATH%%:*}"
6 |
7 | main() {
8 | _cd_into_top_level
9 | _generate_coverage_files
10 | _combine_coverage_reports
11 | }
12 |
13 | _cd_into_top_level() {
14 | cd "$(git rev-parse --show-toplevel)"
15 | #cd nodes/stampzilla-server
16 | }
17 |
18 | _generate_coverage_files() {
19 | for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -not -path '*/stampzilla-telldus-events' -not -path '*/stampzilla-hidcommander' -type d); do
20 | if ls $dir/*.go &>/dev/null ; then
21 | go test -covermode=atomic -coverprofile=$dir/profile.coverprofile $dir || fail=1
22 | fi
23 | done
24 | }
25 |
26 | _install_gover() {
27 | if [[ ! -x "${GOPATH}/bin/gover" ]] ; then
28 | go install github.com/modocache/gover@latest
29 | fi
30 | }
31 |
32 | _combine_coverage_reports() {
33 | _install_gover
34 | ${GOPATH}/bin/gover
35 | mv gover.coverprofile coverage.txt
36 | }
37 |
38 | main "$@"
39 |
--------------------------------------------------------------------------------
/nodes/stampzilla-enocean/handlersinit.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/jonaz/goenocean"
5 | )
6 |
7 | var handlers *eepHandlers
8 |
9 | type eepHandlers struct {
10 | handlers map[string]Handler
11 | }
12 |
13 | type Handler interface {
14 | On(*Device)
15 | Off(*Device)
16 | Toggle(*Device)
17 | Learn(*Device)
18 | Dim(int, *Device)
19 | Process(*Device, goenocean.Telegram)
20 | }
21 |
22 | func (h *eepHandlers) getHandler(t string) Handler {
23 | if handler, ok := h.handlers[t]; ok {
24 | return handler
25 | }
26 | return nil
27 | }
28 |
29 | func init() {
30 | handlers = &eepHandlers{make(map[string]Handler)}
31 | handlers.handlers["a53808"] = &handlerEepa53808{}
32 | handlers.handlers["a53808eltako"] = &handlerEepa53808eltako{}
33 | handlers.handlers["d20109"] = &handlerEepd20109{}
34 | handlers.handlers["a51201"] = &handlerEepa51201{}
35 | handlers.handlers["f60201"] = &handlerEepf60201{}
36 | handlers.handlers["f60201eltako"] = &handlerEepf60201eltako{}
37 | }
38 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/Wrapper.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter as Router } from 'react-router-dom';
2 | import { connect } from 'react-redux';
3 | import React from 'react';
4 |
5 | import App from './App';
6 | import Landing from './Landing';
7 | import Routes from '../routes';
8 | import Websocket from './Websocket';
9 |
10 | const Wrapper = (props) => {
11 | const { server, connection } = props;
12 |
13 | const secure = (window.location.protocol.match(/^https/) || server.get('secure'))
14 | && connection !== 4001;
15 |
16 | return (
17 |
18 |
19 | {!secure && }
20 | {secure && (
21 |
22 |
23 |
24 |
25 |
26 | )}
27 |
28 | );
29 | };
30 |
31 | const mapToProps = state => ({
32 | server: state.get('server'),
33 | connection: state.getIn(['connection', 'code']),
34 | });
35 |
36 | export default connect(mapToProps)(Wrapper);
37 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classnames from 'classnames';
3 |
4 | const Card = (props) => {
5 | const {
6 | title, children, toolbar, bodyClassName, className,
7 | } = props;
8 | return (
9 |
10 |
11 |
{title}
12 |
13 | {toolbar
14 | && toolbar.map((tool) => (
15 |
23 | ))}
24 |
25 |
26 |
{children}
27 |
28 | );
29 | };
30 |
31 | export default Card;
32 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/logic/rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "e8092b86-1261-44cd-ab64-38121df58a79": {
3 | "name": "All off",
4 | "enabled": true,
5 | "active": false,
6 | "uuid": "e8092b86-1261-44cd-ab64-38121df58a79",
7 | "expression": "devices['asdf.123'].on == true && devices['asdf.123'].temperature > 20.0",
8 | "conditions": {
9 | "1fd25327-f43c-4a00-aa67-3969dfed06b5": true
10 | },
11 | "for": "5m",
12 | "actions": [
13 | "1m",
14 | "c7d352bb-23f4-468c-b476-f76599c09a0d"
15 | ]
16 | },
17 | "1fd25327-f43c-4a00-aa67-3969dfed06b5": {
18 | "name": "chromecast p\u00e5",
19 | "enabled": true,
20 | "active": false,
21 | "uuid": "1fd25327-f43c-4a00-aa67-3969dfed06b5",
22 | "expression": "devices['asdf.123'].on == true ",
23 | "conditions": {},
24 | "for": "5m",
25 | "actions": [
26 | "1m",
27 | "c7d352bb-23f4-468c-b476-f76599c09a0d"
28 | ]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/logic.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/logic"
5 | )
6 |
7 | func (store *Store) GetRules() logic.Rules {
8 | store.Logic.RLock()
9 | defer store.Logic.RUnlock()
10 | return store.Logic.Rules
11 | }
12 |
13 | func (store *Store) AddOrUpdateRules(rules logic.Rules) {
14 | store.Logic.SetRules(rules)
15 | store.Logic.Save()
16 | store.runCallbacks("rules")
17 | }
18 |
19 | func (store *Store) GetSavedStates() logic.SavedStates {
20 | return store.Logic.StateStore.All()
21 | }
22 |
23 | func (store *Store) AddOrUpdateSavedStates(s logic.SavedStates) {
24 | store.SavedState.SetState(s)
25 | store.SavedState.Save()
26 | store.runCallbacks("savedstates")
27 | }
28 |
29 | func (store *Store) GetScheduledTasks() logic.Tasks {
30 | return store.Scheduler.Tasks()
31 | }
32 |
33 | func (store *Store) AddOrUpdateScheduledTasks(tasks logic.Tasks) {
34 | store.Scheduler.SetTasks(tasks)
35 | store.Scheduler.Save()
36 | store.runCallbacks("schedules")
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/installer/pidfile.go:
--------------------------------------------------------------------------------
1 | package installer
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | type PidFile string
11 |
12 | func (f *PidFile) String() string {
13 | return string(*f)
14 | }
15 |
16 | // Read the pidfile.
17 | func (f *PidFile) Read() int {
18 | data, err := ioutil.ReadFile(string(*f))
19 | if err != nil {
20 | return 0
21 | }
22 | pidString := strings.Trim(string(data), "\n")
23 | pid, err := strconv.ParseInt(string(pidString), 0, 32)
24 | if err != nil {
25 | return 0
26 | }
27 | return int(pid)
28 | }
29 |
30 | // Write the pidfile.
31 | func (f *PidFile) write(data int) error {
32 | err := ioutil.WriteFile(string(*f), []byte(strconv.Itoa(data)), 0660)
33 | if err != nil {
34 | return err
35 | }
36 | return nil
37 | }
38 |
39 | // Delete the pidfile.
40 | func (f *PidFile) delete() bool {
41 | _, err := os.Stat(string(*f))
42 | if err != nil {
43 | return true
44 | }
45 | err = os.Remove(string(*f))
46 | if err == nil {
47 | return true
48 | }
49 | return false
50 | }
51 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test cover cover-html cover-test
2 |
3 | test:
4 | go test `go list ./... | grep -v /vendor/ | grep -v stampzilla-telldus`
5 |
6 | # todo: use this when golang issue 23910 is resolved
7 | # go test -v -coverpkg=./... -coverprofile=all `go list ./... | grep -v /vendor/ `
8 | cover:
9 | @echo Running coverage
10 | go install github.com/wadey/gocovmerge@latest
11 | $(eval PKGS := $(shell go list ./... | grep -v /vendor/ ))
12 | $(eval PKGS_DELIM := $(shell echo $(PKGS) | sed -e 's/ /,/g'))
13 | go list -f '{{if or (len .TestGoFiles) (len .XTestGoFiles)}}go test -test.v -test.timeout=120s -covermode=atomic -coverprofile={{.Name}}_{{len .Imports}}_{{len .Deps}}.coverprofile -coverpkg $(PKGS_DELIM) {{.ImportPath}}{{end}}' $(PKGS) | xargs -I {} bash -c {}
14 | gocovmerge `ls *.coverprofile` > coverage.txt
15 | rm *.coverprofile
16 | cover-normal:
17 | bash coverage
18 |
19 | cover-html: cover
20 | go tool cover -html coverage.txt
21 |
22 | build-ui:
23 | cd nodes/stampzilla-server/public && gulp
24 | cd nodes/stampzilla-server && go generate
25 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/connections.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models"
4 |
5 | func (store *Store) GetConnections() Connections {
6 | store.RLock()
7 | conns := make(Connections)
8 | for k, v := range store.Connections {
9 | conns[k] = v
10 | }
11 | store.RUnlock()
12 | return conns
13 | }
14 |
15 | func (store *Store) Connection(id string) *models.Connection {
16 | store.RLock()
17 | defer store.RUnlock()
18 | if conn, ok := store.Connections[id]; ok {
19 | return conn
20 | }
21 | return nil
22 | }
23 |
24 | func (store *Store) AddOrUpdateConnection(id string, c *models.Connection) {
25 | store.Lock()
26 | store.Connections[id] = c
27 | store.Unlock()
28 |
29 | store.runCallbacks("connections")
30 | }
31 |
32 | func (store *Store) ConnectionChanged() {
33 | store.runCallbacks("connections")
34 | }
35 |
36 | func (store *Store) RemoveConnection(id string) {
37 | store.Lock()
38 | delete(store.Connections, id)
39 | store.Unlock()
40 | store.runCallbacks("connections")
41 | }
42 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 |
4 | "plugins": [
5 | "react",
6 | "html"
7 | ],
8 |
9 | "env": {
10 | "browser": true
11 | },
12 |
13 | "extends": "airbnb",
14 |
15 | "rules": {
16 | "camelcase": 0,
17 | "function-paren-newline": ["error", "consistent"],
18 | "jsx-a11y/anchor-is-valid": 0,
19 | "jsx-a11y/click-events-have-key-events": 0,
20 | "jsx-a11y/label-has-for": 0,
21 | "jsx-a11y/media-has-caption": 0,
22 | "jsx-a11y/no-autofocus": 0,
23 | "max-len": 0,
24 | "object-curly-newline": ["error", { "minProperties": 4, "consistent": true }],
25 | "react/destructuring-assignment": 0,
26 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
27 | "react/prop-types": 0,
28 | "react/require-default-props": 0,
29 | "jsx-a11y/label-has-associated-control": 0,
30 | },
31 |
32 | "settings": {
33 | "import/resolver": {
34 | "node": {
35 | "paths": ["src"]
36 | }
37 | },
38 | "html/report-bad-indent": "warn"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Stampzilla
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/FormModal.scss:
--------------------------------------------------------------------------------
1 | .formModal {
2 | table {
3 | th {
4 | border-top: 0 !important;
5 | padding-top: 0 !important;
6 | }
7 |
8 | .reason {
9 | border-top: 0;
10 | margin-top: 0;
11 | padding-left: 30px;
12 | background: #eee;
13 | box-shadow: inset 0px 5px 15px -5px rgba(0,0,0,0.75);
14 | }
15 | }
16 | :global(.field-description) {
17 | color: #999;
18 | }
19 | :global(.modal-body) {
20 | dl:last-of-type {
21 | margin-bottom: 0 !important;
22 |
23 | dd:last-of-type {
24 | margin-bottom:0 !important;
25 |
26 | p:last-of-type {
27 | margin-bottom:0 !important;
28 | }
29 | }
30 | }
31 | }
32 | }
33 |
34 | .footerContainer {
35 | .buttons {
36 | display: flex;
37 | align-items: center;
38 | justify-content: flex-end;
39 |
40 | & > button,
41 | & > :global(.btn-group) {
42 | margin-left: 1rem;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jamie Barton
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pkg/installer/binary/releases.go:
--------------------------------------------------------------------------------
1 | package binary
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/google/go-github/github"
7 | )
8 |
9 | func GetReleases() []*github.RepositoryRelease {
10 | client := github.NewClient(nil)
11 | ctx := context.Background()
12 | releases, _, err := client.Repositories.ListReleases(ctx, "stampzilla", "stampzilla-go", &github.ListOptions{})
13 | if err != nil {
14 | return nil
15 | }
16 |
17 | return releases
18 |
19 | ////commits, _, err := client.Repositories.ListCommits(ctx, "stampzilla", "stampzilla-go"
20 |
21 | // url := "https://api.github.com/repos/stampzilla/stampzilla-go/releases"
22 |
23 | // req, err := http.NewRequest("GET", url, nil)
24 | // if err != nil {
25 | // log.Fatal("NewRequest: ", err)
26 | // return []Release{}
27 | //}
28 |
29 | // client := &http.Client{}
30 |
31 | // resp, err := client.Do(req)
32 | // if err != nil {
33 | // log.Fatal("Do: ", err)
34 | // return []Release{}
35 | //}
36 |
37 | // defer resp.Body.Close()
38 |
39 | // var releases []Release
40 |
41 | // if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
42 | // log.Println(err)
43 | //}
44 |
45 | // return releases
46 | }
47 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/pushover/pushover.go:
--------------------------------------------------------------------------------
1 | package pushover
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | pover "github.com/gregdel/pushover"
8 | "github.com/sirupsen/logrus"
9 | )
10 |
11 | var ErrNotImplemented = fmt.Errorf("not implemented")
12 |
13 | type PushOver struct {
14 | Token string `json:"token"`
15 | }
16 |
17 | func New(parameters json.RawMessage) *PushOver {
18 | pb := &PushOver{}
19 |
20 | err := json.Unmarshal(parameters, pb)
21 | if err != nil {
22 | logrus.Error(err)
23 | }
24 | return pb
25 | }
26 |
27 | func (po *PushOver) Release(dest []string, body string) error {
28 | return ErrNotImplemented
29 | }
30 |
31 | func (po *PushOver) Trigger(dest []string, body string) error {
32 | app := pover.New(po.Token)
33 | var err error
34 |
35 | for _, userKey := range dest {
36 | recipient := pover.NewRecipient(userKey)
37 | message := pover.NewMessage(body)
38 | message.Title = "Stampzilla"
39 | _, err = app.SendMessage(message, recipient)
40 | if err != nil {
41 | return err
42 | }
43 | }
44 | return err
45 | }
46 |
47 | func (po *PushOver) Destinations() (map[string]string, error) {
48 | return nil, ErrNotImplemented
49 | }
50 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/server_test.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "sync/atomic"
5 | "testing"
6 |
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestAddOrUpdateServer(t *testing.T) {
12 | store := &Store{
13 | Server: make(map[string]map[string]devices.State),
14 | }
15 |
16 | i := int64(0)
17 | store.OnUpdate(func(str string, s *Store) error {
18 | atomic.AddInt64(&i, 1)
19 | assert.Equal(t, "server", str)
20 | return nil
21 | })
22 |
23 | store.AddOrUpdateServer("area", "item", devices.State{
24 | "state1": true,
25 | })
26 | store.AddOrUpdateServer("area", "item", devices.State{
27 | "state1": true,
28 | })
29 | store.AddOrUpdateServer("area", "item", devices.State{
30 | "state2": true,
31 | })
32 |
33 | assert.Equal(t, int64(2), i)
34 | }
35 |
36 | func TestGetServerStateAsJson(t *testing.T) {
37 | store := &Store{
38 | Server: make(map[string]map[string]devices.State),
39 | }
40 | store.AddOrUpdateServer("area", "item", devices.State{
41 | "state1": true,
42 | })
43 | data := store.GetServerStateAsJson()
44 | assert.Contains(t, string(data), "state1")
45 | }
46 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/routes/home/Clock.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import moment from 'moment'
3 |
4 | class Clock extends React.Component {
5 | constructor(props) {
6 | super(props)
7 | this.clock = React.createRef()
8 | this.seconds = React.createRef()
9 | this.date = React.createRef()
10 | }
11 |
12 | componentDidMount() {
13 | const updateClock = () => {
14 | this.clock.current.innerHTML = moment().format('HH:mm')
15 | this.seconds.current.innerHTML = moment().format('ss')
16 | this.date.current.innerHTML = moment().format('dddd - D MMMM')
17 | }
18 | updateClock()
19 | this.clockInterval = setInterval(updateClock, 1000)
20 | }
21 |
22 | componentWillUnmount() {
23 | clearTimeout(this.clockInterval)
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
30 |
00:00
31 |
32 | 00
33 |
34 |
35 |
36 | -
37 |
38 |
39 | )
40 | }
41 | }
42 |
43 | export default Clock
44 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/ducks/connection.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 |
4 | const c = defineAction(
5 | 'connection',
6 | ['CONNECTED', 'DISCONNECTED', 'ERROR'],
7 | );
8 |
9 | const defaultState = Map({
10 | connected: null,
11 | error: null,
12 | });
13 |
14 | // Actions
15 | export function connected() {
16 | return { type: c.CONNECTED };
17 | }
18 |
19 | export function disconnected() {
20 | return (dispatch, getState) => {
21 | if (getState().getIn(['connection', 'connected']) !== false) {
22 | dispatch({ type: c.DISCONNECTED });
23 | }
24 | };
25 | }
26 |
27 | export function error(err) {
28 | return { type: c.ERROR, error: err };
29 | }
30 |
31 | // Reducer
32 | export default function reducer(state = defaultState, action) {
33 | switch (action.type) {
34 | case c.CONNECTED: {
35 | return state
36 | .set('error', null)
37 | .set('connected', true);
38 | }
39 | case c.DISCONNECTED: {
40 | return state
41 | .set('connected', false);
42 | }
43 | case c.ERROR: {
44 | return state
45 | .set('error', action.error);
46 | }
47 | default: return state;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/routes/home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 |
4 | import Clock from './Clock'
5 | import Forecast from './Forecast'
6 | import DeviceList from './DeviceList'
7 |
8 | class Home extends React.PureComponent {
9 | render() {
10 | const { widgets } = this.props
11 | return (
12 |
13 | {widgets &&
14 | widgets.map((widget, index) => {
15 | switch (widget.get('type')) {
16 | case 'clock':
17 | return
18 | case 'forecast':
19 | return
20 | case 'devicelist':
21 | return (
22 |
26 | )
27 | default:
28 | return null
29 | }
30 | })}
31 |
32 | )
33 | }
34 | }
35 |
36 | const mapStateToProps = state => ({
37 | widgets: state.getIn(['config', 'widgets'])
38 | })
39 |
40 | export default connect(mapStateToProps)(Home)
41 |
--------------------------------------------------------------------------------
/nodes/stampzilla-chromecast/state.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type State struct {
8 | Chromecasts map[string]*Chromecast
9 | sync.RWMutex
10 | }
11 |
12 | func (s *State) GetByUUID(uuid string) *Chromecast {
13 | s.RLock()
14 | defer s.RUnlock()
15 | if val, ok := s.Chromecasts[uuid]; ok {
16 | return val
17 | }
18 | return nil
19 | }
20 |
21 | func (s *State) Add(c *Chromecast) {
22 | s.Lock()
23 | s.Chromecasts[c.Uuid()] = c
24 | s.Unlock()
25 |
26 | /*
27 | s.node.Chromecasts.Add(&devices.Device{
28 | Type: "chromecast",
29 | Name: c.Name(),
30 | Id: c.Id,
31 | Online: true,
32 | StateMap: map[string]string{
33 | "Playing": c.Id + ".Playing",
34 | "PrimaryApp": c.Id + ".PrimaryApp",
35 | "Title": c.Id + ".Media.Title",
36 | "SubTitle": c.Id + ".Media.SubTitle",
37 | "Thumb": c.Id + ".Media.Thumb",
38 | "Url": c.Id + ".Media.Url",
39 | "Duration": c.Id + ".Media.Duration",
40 | },
41 | })
42 | */
43 | }
44 |
45 | // Remove removes a chromecast from the state.
46 | func (s *State) Remove(c *Chromecast) {
47 | _, ok := s.Chromecasts[c.Uuid()]
48 | if ok {
49 | s.Lock()
50 | delete(s.Chromecasts, c.Uuid())
51 | s.Unlock()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | * [Devices](devices.md)
4 | * [Screenshots](screenshots.md)
5 |
6 |
7 | ## Architecture
8 |
9 | stampzilla-go is composed of alot of standalone services or nodes as we call them (nowdays they are commonly known as microservices).
10 | There is a cli which can be used to start, stop and show status of the running services.
11 | available official nodes can be found in the nodes directory.
12 |
13 | All nodes report their state to the server node which have schedule and rule engines. The server also have capabilities to log all state changes as metrics to influxdb which mean you can easilly draw temperature graphs or see how long a lamp has been on over time.
14 |
15 |
16 | ## Nodes
17 |
18 | Nodes can be configured in the web interface. The config is a json object and default config for each node can be found in the links below.
19 | Its up to the node if it requires config or not. For example telldus does not require any config but takes all config from the telldusd running on the same machine.
20 |
21 | * [deconz](../nodes/stampzilla-deconz/README.md)
22 | * [google-assistant](../nodes/stampzilla-google-assistant/README.md)
23 | * [server](../nodes/stampzilla-server/README.md)
24 | * [telldus](../nodes/stampzilla-telldus/README.md)
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/nodes/stampzilla-husdata-h60/model_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestHeatPumpState(t *testing.T) {
11 | tests := []struct {
12 | in string
13 | out string
14 | }{
15 | // 65529 - 65536 = -7
16 | {`{"0007":65529}`, `"Outdoor":-0.7`},
17 | {`{"0007":-7}`, `"Outdoor":-0.7`},
18 | {`{"0007":-71}`, `"Outdoor":-7.1`},
19 | {`{"0007":123}`, `"Outdoor":12.3`},
20 | {`{"1A01":1}`, `"Compressor":true`},
21 | {`{"1A01":0}`, `"Compressor":false`},
22 | {`{"5C52":844329}`, `"SuppliedEnergyHeating":8443.29`},
23 | {`{"5C53":463482}`, `"SuppliedEnergyHotwater":4634.82`},
24 | {`{"5C55":4758500}`, `"CompressorConsumptionHeating":4758.5`},
25 | {`{"5C56":2736630}`, `"CompressorConsumptionHotwater":2736.63`},
26 | {`{"5C58":3608}`, `"AuxConsumptionHeating":3.608`},
27 | {`{"5C59":250314}`, `"AuxConsumptionHotwater":250.314`},
28 | }
29 |
30 | for _, tt := range tests {
31 | data := tt
32 | t.Run(data.in, func(t *testing.T) {
33 | hp := &HeatPump{}
34 | err := json.Unmarshal([]byte(data.in), hp)
35 | assert.NoError(t, err)
36 | out, err := json.Marshal(hp.State())
37 | assert.NoError(t, err)
38 | assert.Contains(t, string(out), data.out)
39 | })
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/ErrorBoundary.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class ErrorBoundary extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | this.state = { hasError: false };
7 | }
8 |
9 | componentDidCatch(error, info) {
10 | this.setState({
11 | hasError: true,
12 | error,
13 | info,
14 | });
15 | }
16 |
17 | render() {
18 | const { hasError, error, info } = this.state;
19 |
20 | if (hasError) {
21 | return (
22 |
23 |
Something went wrong.
24 |
{error.toString()}
25 |
26 |
Stack trace
27 |
{error.stack.replace(/webpack:\/\/\/.\//g, '')}
28 | {info && (
29 |
30 | Component trace
31 | {info.componentStack.replace(/^\s+|\s+$/g, '')}
32 |
33 | )}
34 |
35 | );
36 | }
37 |
38 | return this.props.children;
39 | }
40 | }
41 |
42 | export default ErrorBoundary;
43 |
44 | export const withBoudary = WrappedComponent => props => (
45 |
46 |
47 |
48 | );
49 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/helpers/isprivateip.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "net/url"
7 | )
8 |
9 | var privateIPBlocks []*net.IPNet
10 |
11 | // Credits to https://stackoverflow.com/questions/41240761/check-if-ip-address-is-in-private-network-space/50825191#50825191
12 | func init() {
13 | for _, cidr := range []string{
14 | "127.0.0.0/8", // IPv4 loopback
15 | "10.0.0.0/8", // RFC1918
16 | "172.16.0.0/12", // RFC1918
17 | "192.168.0.0/16", // RFC1918
18 | "169.254.0.0/16", // RFC3927 link-local
19 | "::1/128", // IPv6 loopback
20 | "fe80::/10", // IPv6 link-local
21 | "fc00::/7", // IPv6 unique local addr
22 | } {
23 | _, block, err := net.ParseCIDR(cidr)
24 | if err != nil {
25 | panic(fmt.Errorf("parse error on %q: %v", cidr, err))
26 | }
27 | privateIPBlocks = append(privateIPBlocks, block)
28 | }
29 | }
30 |
31 | func IsPrivateIP(ipStr string) bool {
32 | u, err := url.Parse("http://" + ipStr)
33 | if err != nil {
34 | return false
35 | }
36 |
37 | ip := net.ParseIP(u.Hostname())
38 |
39 | if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
40 | return true
41 | }
42 |
43 | for _, block := range privateIPBlocks {
44 | if block.Contains(ip) {
45 | return true
46 | }
47 | }
48 | return false
49 | }
50 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/node.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "encoding/json"
5 | "sync"
6 |
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
8 | "github.com/stampzilla/stampzilla-go/v2/pkg/build"
9 | )
10 |
11 | type Node struct {
12 | UUID string `json:"uuid,omitempty"`
13 | Connected_ bool `json:"connected,omitempty"`
14 | Build build.Data `json:"build,omitempty"`
15 | Type string `json:"type,omitempty"`
16 | Name string `json:"name,omitempty"`
17 | // Devices Devices `json:"devices,omitempty"`
18 | Config json.RawMessage `json:"config,omitempty"`
19 | Aliases map[devices.ID]string `json:"aliases,omitempty"`
20 | sync.Mutex
21 | }
22 |
23 | func (n *Node) SetConnected(c bool) {
24 | n.Lock()
25 | n.Connected_ = c
26 | n.Unlock()
27 | }
28 |
29 | func (n *Node) Connected() bool {
30 | n.Lock()
31 | defer n.Unlock()
32 | return n.Connected_
33 | }
34 |
35 | func (n *Node) SetAlias(id devices.ID, alias string) {
36 | n.Lock()
37 | if n.Aliases == nil {
38 | n.Aliases = make(map[devices.ID]string)
39 | }
40 | n.Aliases[id] = alias
41 | n.Unlock()
42 | }
43 |
44 | func (n *Node) Alias(id devices.ID) string {
45 | n.Lock()
46 | defer n.Unlock()
47 | if a, ok := n.Aliases[id]; ok {
48 | return a
49 | }
50 | return ""
51 | }
52 |
--------------------------------------------------------------------------------
/nodes/stampzilla-mbus/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "host": "192.168.13.42",
3 | "port": "10001",
4 | "devices": [
5 | {
6 | "name": "Electrical Meter",
7 | "interval": "30s",
8 | "primaryAddress": 1,
9 | "Frames": {
10 | "0": [
11 | {
12 | "id": 0,
13 | "name": "total"
14 | },
15 | {
16 | "id": 2,
17 | "name": "current"
18 | }
19 | ]
20 | }
21 | },
22 | {
23 | "name": "Coldwater",
24 | "interval": "10s",
25 | "primaryAddress": 2,
26 | "Frames": {
27 | "0": [
28 | {
29 | "id": 6,
30 | "name": "total"
31 | }
32 | ]
33 | }
34 | },
35 | {
36 | "name": "Hotwater",
37 | "interval": "10s",
38 | "primaryAddress": 3,
39 | "Frames": {
40 | "0": [
41 | {
42 | "id": 6,
43 | "name": "total"
44 | }
45 | ]
46 | }
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/nodes/stampzilla-1wire/onewire/1wire.go:
--------------------------------------------------------------------------------
1 | package onewire
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | var ErrSensorRead = errors.New("failed to read temperature from sensor")
12 |
13 | func SensorsWithTemperature() ([]string, error) {
14 | data, err := ioutil.ReadFile("/sys/bus/w1/devices/w1_bus_master1/w1_master_slaves")
15 | if err != nil {
16 | return nil, fmt.Errorf("error reading 1wire from sys filesystem: %w", err)
17 | }
18 | sensors := []string{}
19 | for _, s := range strings.Split(string(data), "\n") {
20 | s = strings.TrimSpace(s)
21 | if _, err := Temperature(s); err != nil {
22 | continue
23 | }
24 | sensors = append(sensors, s)
25 | }
26 | return sensors, nil
27 | }
28 |
29 | // Temperature get the temp of sensor with id.
30 | func Temperature(sensor string) (float64, error) {
31 | data, err := ioutil.ReadFile("/sys/bus/w1/devices/" + sensor + "/w1_slave")
32 | if err != nil {
33 | return 0.0, ErrSensorRead
34 | }
35 |
36 | raw := string(data)
37 |
38 | if !strings.Contains(raw, " YES") {
39 | return 0.0, ErrSensorRead
40 | }
41 |
42 | i := strings.LastIndex(raw, "t=")
43 | if i == -1 {
44 | return 0.0, ErrSensorRead
45 | }
46 |
47 | c, err := strconv.ParseFloat(raw[i+2:len(raw)-1], 64)
48 | if err != nil {
49 | return 0.0, ErrSensorRead
50 | }
51 |
52 | return c / 1000.0, nil
53 | }
54 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/email/email.go:
--------------------------------------------------------------------------------
1 | package email
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/smtp"
7 | )
8 |
9 | type EmailSender struct {
10 | Server string `json:"server"`
11 | Port int `json:"port"`
12 | From string `json:"from"`
13 | Password string `json:"password"`
14 | send func(string, smtp.Auth, string, []string, []byte) error
15 | }
16 |
17 | func New(parameters json.RawMessage) *EmailSender {
18 | es := &EmailSender{send: smtp.SendMail}
19 |
20 | json.Unmarshal(parameters, es)
21 |
22 | return es
23 | }
24 |
25 | func (es *EmailSender) Trigger(dest []string, body string) error {
26 | return es.notify(true, dest, body)
27 | }
28 |
29 | func (es *EmailSender) Release(dest []string, body string) error {
30 | return es.notify(false, dest, body)
31 | }
32 |
33 | func (es *EmailSender) notify(trigger bool, dest []string, body string) error {
34 | event := "Triggered"
35 | if !trigger {
36 | event = "Released"
37 | }
38 |
39 | msg := "From: " + es.From + "\n" +
40 | "Subject: stampzilla - " + event + "\n\n" +
41 | body
42 |
43 | return es.send(fmt.Sprintf("%s:%d", es.Server, es.Port),
44 | smtp.PlainAuth("", es.From, es.Password, es.Server),
45 | es.From, dest, []byte(msg))
46 | }
47 |
48 | func (es *EmailSender) Destinations() (map[string]string, error) {
49 | return nil, fmt.Errorf("not implemented")
50 | }
51 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "homepage": "https://create-react-app-redux.now.sh",
4 | "scripts": {
5 | "deploy": "now && now alias",
6 | "start": "react-scripts start",
7 | "now-start": "serve -s ./build",
8 | "build": "react-scripts build",
9 | "test": "react-scripts test --env=jsdom",
10 | "eject": "react-scripts eject",
11 | "precommit": "pretty-quick --staged"
12 | },
13 | "devDependencies": {
14 | "prettier": "1.17.0",
15 | "react-scripts": "^5.0.1",
16 | "terser": "^4.8.1"
17 | },
18 | "dependencies": {
19 | "axios": "^0.21.2",
20 | "connected-react-router": "6.4.0",
21 | "moment": "^2.29.4",
22 | "react": "16.8.6",
23 | "react-dom": "16.8.6",
24 | "react-moment": "^0.9.2",
25 | "react-redux": "7.0.3",
26 | "react-router": "5.0.0",
27 | "react-router-dom": "5.0.0",
28 | "react-skycons": "^0.7.0",
29 | "reconnectingwebsocket": "^1.0.0",
30 | "redux": "4.0.1",
31 | "redux-define": "^1.1.1",
32 | "redux-immutable": "^4.0.0",
33 | "redux-thunk": "2.3.0",
34 | "sanitize.css": "8.0.0",
35 | "serve": "14.1.2",
36 | "yr.no-forecast": "^2.1.0"
37 | },
38 | "resolutions": {
39 | "request": "^2.88.0"
40 | },
41 | "browserslist": [
42 | ">0.2%",
43 | "not dead",
44 | "not ie <= 11",
45 | "not op_mini all"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/webhook/webhook_test.go:
--------------------------------------------------------------------------------
1 | package webhook
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestNew(t *testing.T) {
13 | sender := New(json.RawMessage("{\"method\": \"method1\"}"))
14 |
15 | assert.Equal(t, "method1", sender.Method)
16 | }
17 |
18 | func TestTrigger(t *testing.T) {
19 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
20 | assert.Equal(t, "/webhook", req.URL.String())
21 | rw.Write([]byte(`OK`))
22 | }))
23 | defer server.Close()
24 |
25 | sender := New(json.RawMessage("{\"method\": \"PUT\"}"))
26 | err := sender.Trigger([]string{server.URL + "/webhook"}, "")
27 |
28 | assert.NoError(t, err)
29 | }
30 |
31 | func TestRelease(t *testing.T) {
32 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
33 | assert.Equal(t, "/webhook", req.URL.String())
34 | rw.Write([]byte(`OK`))
35 | }))
36 | defer server.Close()
37 |
38 | sender := New(json.RawMessage("{\"method\": \"PUT\"}"))
39 | err := sender.Release([]string{server.URL + "/webhook"}, "")
40 |
41 | assert.NoError(t, err)
42 | }
43 |
44 | func TestDestinations(t *testing.T) {
45 | sender := New(json.RawMessage("{\"method\": \"PUT\"}"))
46 | d, err := sender.Destinations()
47 | assert.Nil(t, d)
48 | assert.Error(t, err)
49 | }
50 |
--------------------------------------------------------------------------------
/nodes/stampzilla-nibe/nibe/parameters.go:
--------------------------------------------------------------------------------
1 | package nibe
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "strconv"
9 | )
10 |
11 | type Parameter struct {
12 | Register string `json:"register"`
13 | Factor int `json:"factor"`
14 | Size string `json:"size"`
15 | Mode string `json:"mode"`
16 | Title string `json:"titel"`
17 | Info string `json:"info"`
18 | Unit string `json:"unit"`
19 | Min string `json:"min"`
20 | Max string `json:"max"`
21 | Map map[string]string `json:"map"`
22 | }
23 |
24 | var parameters = make(map[int]Parameter)
25 |
26 | func (n *Nibe) LoadDefinitions(statikFS http.FileSystem, filename string) error {
27 | jsonFile, err := statikFS.Open(filename)
28 | if err != nil {
29 | return err
30 | }
31 | defer jsonFile.Close()
32 |
33 | byteValue, err := ioutil.ReadAll(jsonFile)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | var decodedParams []Parameter
39 | json.Unmarshal(byteValue, &decodedParams)
40 |
41 | for _, param := range decodedParams {
42 | id, _ := strconv.Atoi(param.Register)
43 | parameters[id] = param
44 | }
45 |
46 | return nil
47 | }
48 |
49 | func (n *Nibe) Describe(reg uint16) (*Parameter, error) {
50 | if p, ok := parameters[int(reg)]; ok {
51 | return &p, nil
52 | }
53 |
54 | return nil, fmt.Errorf("Not found")
55 | }
56 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/src/routes/home/DeviceList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 |
4 | class DeviceList extends React.PureComponent {
5 | render() {
6 | const { devices, state } = this.props
7 |
8 | return (
9 |
10 | {devices.map(device => {
11 | let value = JSON.stringify(
12 | state.getIn([device.get('device'), 'state', device.get('state')])
13 | )
14 |
15 | if (value && device.get('decimals')) {
16 | value *= Math.pow(10, device.get('decimals'))
17 | value = Math.round(value)
18 | value /= Math.pow(10, device.get('decimals'))
19 | }
20 |
21 | if (value && device.get('states')) {
22 | value = device.getIn(['states', value])
23 | }
24 |
25 | if (device.get('unit')) {
26 | value = `${value} ${device.get('unit')}`
27 | }
28 |
29 | return (
30 |
33 | {value}
34 | {device.get('title')}
35 |
36 | )
37 | })}
38 |
39 | )
40 | }
41 | }
42 |
43 | const mapStateToProps = state => ({
44 | state: state.getIn(['devices', 'list'])
45 | })
46 |
47 | export default connect(mapStateToProps)(DeviceList)
48 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/webhook/webhook.go:
--------------------------------------------------------------------------------
1 | package webhook
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | )
8 |
9 | type WebhookSender struct {
10 | Method string `json:"method"`
11 | }
12 |
13 | func New(parameters json.RawMessage) *WebhookSender {
14 | ws := &WebhookSender{}
15 |
16 | json.Unmarshal(parameters, ws)
17 |
18 | return ws
19 | }
20 |
21 | func (ws *WebhookSender) Trigger(dest []string, body string) error {
22 | var failure error
23 | for _, url := range dest {
24 | err := ws.notify(true, url, body)
25 | if err != nil {
26 | failure = err
27 | }
28 | }
29 |
30 | return failure
31 | }
32 |
33 | func (ws *WebhookSender) Release(dest []string, body string) error {
34 | var failure error
35 | for _, url := range dest {
36 | err := ws.notify(false, url, body)
37 | if err != nil {
38 | failure = err
39 | }
40 | }
41 |
42 | return failure
43 | }
44 |
45 | func (ws *WebhookSender) notify(trigger bool, url string, body string) error {
46 | req, err := http.NewRequest(ws.Method, url, nil)
47 | if err != nil {
48 | return err
49 | }
50 |
51 | client := &http.Client{}
52 | resp, err := client.Do(req)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | defer resp.Body.Close()
58 |
59 | // b, err := ioutil.ReadAll(resp.Body)
60 | // spew.Dump(b)
61 |
62 | return err
63 | }
64 |
65 | func (ws *WebhookSender) Destinations() (map[string]string, error) {
66 | return nil, fmt.Errorf("not implemented")
67 | }
68 |
--------------------------------------------------------------------------------
/nodes/stampzilla-linux/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "strings"
6 |
7 | "github.com/sirupsen/logrus"
8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
9 | "github.com/stampzilla/stampzilla-go/v2/pkg/node"
10 | )
11 |
12 | type Config struct {
13 | Display string `json:"display"`
14 | Players map[string]PlayerConfig `json:"players"`
15 | }
16 |
17 | var (
18 | config Config
19 | n *node.Node
20 | )
21 |
22 | func main() {
23 | n = node.New("linux")
24 |
25 | n.OnConfig(updatedConfig)
26 | n.OnRequestStateChange(func(state devices.State, device *devices.Device) error {
27 | logrus.Info("OnRequestStateChange:", state, device.ID)
28 |
29 | devID := strings.Split(device.ID.ID, ":")
30 | switch devID[0] {
31 | case "monitor":
32 | return changeDpmsState(":"+devID[1], state["on"] == true)
33 | case "player":
34 | return commandPlayer(devID[1], state["on"] == true)
35 | case "audio":
36 | return commandVolume(state)
37 | }
38 |
39 | return nil
40 | })
41 |
42 | err := n.Connect()
43 | if err != nil {
44 | logrus.Error(err)
45 | return
46 | }
47 |
48 | go startMonitorDpms()
49 | go monitorHealth()
50 | go monitorVolume()
51 |
52 | n.Wait()
53 | }
54 |
55 | func updatedConfig(data json.RawMessage) error {
56 | newConf := Config{}
57 | err := json.Unmarshal(data, &newConf)
58 | if err != nil {
59 | return err
60 | }
61 |
62 | config = newConf
63 |
64 | restartPlayers()
65 |
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/message.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/lesismal/melody"
8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/interfaces"
9 | )
10 |
11 | type Message struct {
12 | FromUUID string `json:"fromUUID,omitempty"`
13 | Type string `json:"type"`
14 | Body json.RawMessage `json:"body,omitempty"`
15 | Request json.RawMessage `json:"request,omitempty"`
16 | }
17 |
18 | func NewMessage(t string, body interface{}) (*Message, error) {
19 | b, err := json.Marshal(body)
20 | if err != nil {
21 | return nil, err
22 | }
23 |
24 | return &Message{
25 | Type: t,
26 | Body: json.RawMessage(b),
27 | }, nil
28 | }
29 |
30 | func ParseMessage(msg []byte) (*Message, error) {
31 | data := &Message{}
32 | err := json.Unmarshal(msg, data)
33 | return data, err
34 | }
35 |
36 | func (m *Message) WriteTo(s interfaces.MelodyWriter) error {
37 | msg, err := m.Encode()
38 | if err != nil {
39 | return err
40 | }
41 |
42 | return s.Write(msg)
43 | }
44 |
45 | func (m *Message) WriteWithFilter(mel *melody.Melody, f func(s *melody.Session) bool) error {
46 | msg, err := m.Encode()
47 | if err != nil {
48 | return err
49 | }
50 |
51 | return mel.BroadcastFilter(msg, f)
52 | }
53 |
54 | func (m *Message) Encode() ([]byte, error) {
55 | msg, err := json.Marshal(m)
56 |
57 | return msg, err
58 | }
59 |
60 | func (m *Message) String() string {
61 | return fmt.Sprintf("Type: %s Body: %s", m.Type, string(m.Body))
62 | }
63 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/savedstates.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 | import { v4 as makeUUID } from 'uuid';
4 |
5 | const c = defineAction('savedstates', ['ADD', 'SAVE', 'UPDATE', 'REMOVE']);
6 |
7 | const defaultState = Map({
8 | list: Map(),
9 | });
10 |
11 | // Actions
12 | export function add(states) {
13 | return { type: c.ADD, states };
14 | }
15 | export function save(state) {
16 | return { type: c.SAVE, state };
17 | }
18 | export function remove(uuid) {
19 | return { type: c.REMOVE, uuid };
20 | }
21 | export function update(states) {
22 | return { type: c.UPDATE, states };
23 | }
24 |
25 | // Subscribe to channels and register the action for the packages
26 | export function subscribe(dispatch) {
27 | return {
28 | savedstates: (states) => dispatch(update(states)),
29 | };
30 | }
31 |
32 | // Reducer
33 | export default function reducer(state = defaultState, action) {
34 | switch (action.type) {
35 | case c.ADD: {
36 | const s = {
37 | ...action.state,
38 | uuid: makeUUID(),
39 | };
40 | return state.setIn(['list', s.uuid], fromJS(s));
41 | }
42 | case c.SAVE: {
43 | return state.mergeIn(['list', action.state.uuid], fromJS(action.state));
44 | }
45 | case c.REMOVE: {
46 | return state.deleteIn(['list', action.uuid]);
47 | }
48 | case c.UPDATE: {
49 | return state.set('list', fromJS(action.states));
50 | }
51 | default:
52 | return state;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/persons.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 | import { v4 as makeUUID } from 'uuid';
4 |
5 | const c = defineAction('persons', ['ADD', 'SAVE', 'REMOVE', 'UPDATE']);
6 |
7 | const defaultState = Map({
8 | list: Map(),
9 | });
10 |
11 | // Actions
12 | export function add(person) {
13 | return { type: c.ADD, person };
14 | }
15 | export function save(person) {
16 | return { type: c.SAVE, person };
17 | }
18 | export function remove(uuid) {
19 | return { type: c.REMOVE, uuid };
20 | }
21 | export function update(list) {
22 | return { type: c.UPDATE, list };
23 | }
24 |
25 | // Subscribe to channels and register the action for the packages
26 | export function subscribe(dispatch) {
27 | return {
28 | persons: (persons) => dispatch(update(persons)),
29 | };
30 | }
31 |
32 | // Reducer
33 | export default function reducer(state = defaultState, action) {
34 | switch (action.type) {
35 | case c.ADD: {
36 | const person = {
37 | ...action.person,
38 | uuid: makeUUID(),
39 | };
40 | return state.setIn(['list', person.uuid], fromJS(person));
41 | }
42 | case c.SAVE: {
43 | return state.mergeIn(['list', action.person.uuid], fromJS(action.person));
44 | }
45 | case c.REMOVE: {
46 | return state.deleteIn(['list', action.uuid]);
47 | }
48 | case c.UPDATE: {
49 | return state.set('list', fromJS(action.list));
50 | }
51 | default:
52 | return state;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/nodes/stampzilla-deconz/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/pkg/errors"
9 | "github.com/sirupsen/logrus"
10 | )
11 |
12 | var config = &Config{}
13 |
14 | type Config struct {
15 | IP string
16 | Port string
17 | WsPort string
18 | Password string
19 | }
20 |
21 | var localConfig = &LocalConfig{}
22 |
23 | type LocalConfig struct {
24 | APIKey string
25 | }
26 |
27 | func (lc LocalConfig) Save() error {
28 | path := "localconfig.json"
29 | configFile, err := os.Create(path)
30 | if err != nil {
31 | return fmt.Errorf("error saving local config: %s", err.Error())
32 | }
33 | encoder := json.NewEncoder(configFile)
34 | encoder.SetIndent("", "\t")
35 | err = encoder.Encode(lc)
36 | return errors.Wrap(err, "error saving local config")
37 | }
38 |
39 | func (lc *LocalConfig) Load() error {
40 | path := "localconfig.json"
41 | logrus.Debug("loading local config from ", path)
42 | configFile, err := os.Open(path)
43 | if err != nil {
44 | if os.IsNotExist(err) {
45 | logrus.Warn(err)
46 | return nil
47 | }
48 | return fmt.Errorf("error loading local config: %s", err.Error())
49 | }
50 |
51 | jsonParser := json.NewDecoder(configFile)
52 | err = jsonParser.Decode(&lc)
53 | return errors.Wrap(err, "error loading local config")
54 |
55 | // TODO loop over rules and generate UUIDs if needed. If it was needed save the rules again
56 | }
57 |
58 | /*
59 | Config to put into gui:
60 | {
61 | "ip":"192.168.13.1",
62 | "port":"9042",
63 | "password":"password"
64 | }
65 |
66 | */
67 |
--------------------------------------------------------------------------------
/nodes/stampzilla-nx-witness/model.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | )
7 |
8 | type Rule struct {
9 | ActionParams string `json:"actionParams"`
10 | ActionResourceIds []string `json:"actionResourceIds"`
11 | ActionType string `json:"actionType"`
12 | AggregationPeriod int `json:"aggregationPeriod"`
13 | Comment string `json:"comment"`
14 | Disabled bool `json:"disabled"`
15 | EventCondition string `json:"eventCondition"`
16 | EventResourceIds []string `json:"eventResourceIds"`
17 | EventState string `json:"eventState"`
18 | EventType string `json:"eventType"`
19 | ID string `json:"id"`
20 | Schedule string `json:"schedule"`
21 | System bool `json:"system"`
22 | }
23 |
24 | type EventRulesResponse []Rule
25 |
26 | var ErrRuleNotFound = fmt.Errorf("rule not found")
27 |
28 | func (rules EventRulesResponse) ByID(id string) (Rule, error) {
29 | for _, v := range rules {
30 | if v.StampzillaDeviceID() == id {
31 | return v, nil
32 | }
33 | }
34 | return Rule{}, ErrRuleNotFound
35 | }
36 |
37 | /*
38 | those are mandatory when using POST
39 | id uuid (required)
40 | eventType enum (required)
41 | eventState enum (required)
42 | actionType enum (required)
43 | disabled boolean (required)
44 | */
45 |
46 | var re = regexp.MustCompile(`stampzilla:(.*)`)
47 |
48 | func (r Rule) StampzillaDeviceID() string {
49 | m := re.FindAllStringSubmatch(r.Comment, 1)
50 | if len(m) > 0 && len(m[0]) > 1 {
51 | return m[0][1]
52 | }
53 | return ""
54 | }
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | stampzilla-go [](https://travis-ci.org/stampzilla/stampzilla-go) [](https://codecov.io/gh/stampzilla/stampzilla-go) [](https://goreportcard.com/report/github.com/stampzilla/stampzilla-go)
2 | =============
3 |
4 | Awesome homeautomation software written in Go and React
5 |
6 | ### Installing
7 |
8 | Installation from precompiled binaries
9 | ```bash
10 | curl -s https://api.github.com/repos/stampzilla/stampzilla-go/releases/latest | grep "browser_download_url.*stampzilla-linux-amd64" | cut -d : -f 2,3 | tr -d \" | xargs curl -L -s -o stampzilla && chmod +x stampzilla
11 | sudo mv stampzilla /usr/local/bin #or ~/bin if you use that
12 | sudo stampzilla install server deconz #or whatever nodes you want to use.
13 | ```
14 |
15 | Installation from source
16 | ```bash
17 | go install github.com/stampzilla/stampzilla-go/v2/cmd/stampzilla@latest
18 | sudo stampzilla install
19 | ```
20 | This creates a stampzilla user. checksout the code in stampzilla user home folder and creates some required folders.
21 |
22 | ### Updating
23 |
24 | Update the cli with `stampzilla self-update`
25 |
26 | Update nodes with
27 | ```
28 | sudo stampzilla install -u
29 | sudo stampzilla restart
30 | ```
31 |
32 | ### Documentation
33 | Is work in progress and can be found here:
34 | * [Docs](docs/README.md)
35 | * [Screenshots](docs/screenshots.md)
36 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/index.js:
--------------------------------------------------------------------------------
1 | import 'bootstrap/dist/js/bootstrap.bundle';
2 | import 'admin-lte/dist/js/adminlte';
3 | import 'core-js/stable';
4 | import 'regenerator-runtime/runtime';
5 | import { Provider } from 'react-redux';
6 | import { render } from 'react-dom';
7 | import React from 'react';
8 | import { ToastContainer } from 'react-toastify';
9 |
10 | import './index.scss';
11 | import './images/android-chrome-192x192.png';
12 | import './images/android-chrome-512x512.png';
13 | import './images/apple-touch-icon.png';
14 | import './images/browserconfig.xml';
15 | import './images/favicon-16x16.png';
16 | import './images/favicon-32x32.png';
17 | import './images/favicon.ico';
18 | import './images/safari-pinned-tab.svg';
19 | import './images/site.webmanifest';
20 | import ErrorBoundary from './components/ErrorBoundary';
21 | import Wrapper from './components/Wrapper';
22 | import store from './store';
23 |
24 | render(
25 |
26 |
27 |
28 |
29 |
30 | ,
31 | document.getElementById('app'),
32 | );
33 |
34 | /* eslint-disable no-undef */
35 | if (NODE_ENV === 'production') {
36 | (function registerServiceWorker() {
37 | if ('serviceWorker' in navigator) {
38 | navigator.serviceWorker
39 | .register('service-worker.js', { scope: '/' })
40 | .then(() => console.log("Service Worker registered successfully.")) // eslint-disable-line
41 | .catch((error) => console.log('Service Worker registration failed:', error),
42 | ); // eslint-disable-line
43 | }
44 | }());
45 | }
46 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/senders.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 | import { v4 as makeUUID } from 'uuid';
4 |
5 | const c = defineAction(
6 | 'senders',
7 | ['ADD', 'SAVE', 'UPDATE', 'UPDATE_STATE'],
8 | );
9 |
10 | const defaultState = Map({
11 | list: Map(),
12 | });
13 |
14 | // Actions
15 | export function add(sender) {
16 | return { type: c.ADD, sender };
17 | }
18 | export function save(sender) {
19 | return { type: c.SAVE, sender };
20 | }
21 | export function update(senders) {
22 | return { type: c.UPDATE, senders };
23 | }
24 | export function updateState(senders) {
25 | return { type: c.UPDATE_STATE, senders };
26 | }
27 |
28 | // Subscribe to channels and register the action for the packages
29 | export function subscribe(dispatch) {
30 | return {
31 | senders: senders => dispatch(update(senders)),
32 | };
33 | }
34 |
35 | // Reducer
36 | export default function reducer(state = defaultState, action) {
37 | switch (action.type) {
38 | case c.ADD: {
39 | const sender = {
40 | ...action.sender,
41 | uuid: makeUUID(),
42 | };
43 | return state
44 | .setIn(['list', sender.uuid], fromJS(sender));
45 | }
46 | case c.SAVE: {
47 | return state
48 | .mergeIn(['list', action.sender.uuid], fromJS(action.sender));
49 | }
50 | case c.UPDATE: {
51 | return state
52 | .set('list', fromJS(action.senders));
53 | }
54 | case c.UPDATE_STATE: {
55 | return state
56 | .set('state', fromJS(action.senders));
57 | }
58 | default: return state;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/nodes/stampzilla-linux/volume.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | volume "github.com/itchyny/volume-go"
8 | "github.com/sirupsen/logrus"
9 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
10 | )
11 |
12 | func monitorVolume() {
13 | dev := &devices.Device{
14 | Name: "Audio",
15 | ID: devices.ID{ID: "audio"},
16 | Online: true,
17 | Traits: []string{"OnOff", "Volume"},
18 | State: devices.State{
19 | "on": false,
20 | "volume": 0,
21 | },
22 | }
23 | added := false
24 |
25 | for {
26 | vol, err := volume.GetVolume()
27 | if err != nil {
28 | logrus.Errorf("get volume failed: %+v", err)
29 | return
30 | }
31 |
32 | mute, err := volume.GetMuted()
33 | if err != nil {
34 | logrus.Errorf("get mute failed: %+v", err)
35 | return
36 | }
37 |
38 | if !added {
39 | n.AddOrUpdate(dev)
40 | }
41 |
42 | newState := make(devices.State)
43 | newState["on"] = !mute
44 | newState["volume"] = float64(vol) / 100
45 | n.UpdateState(dev.ID.ID, newState)
46 | <-time.After(time.Second * 1)
47 | }
48 | }
49 |
50 | func commandVolume(state devices.State) error {
51 | if state["volume"] != nil {
52 | err := volume.SetVolume(int(state["volume"].(float64) * 100))
53 | if err != nil {
54 | return fmt.Errorf("set volume failed: %+v", err)
55 | }
56 | }
57 |
58 | if state["on"] == true {
59 | err := volume.Unmute()
60 | if err != nil {
61 | return fmt.Errorf("mute failed: %+v", err)
62 | }
63 | } else if state["on"] == false {
64 | err := volume.Mute()
65 | if err != nil {
66 | return fmt.Errorf("unmute failed: %+v", err)
67 | }
68 | }
69 |
70 | return nil
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/node/mdns.go:
--------------------------------------------------------------------------------
1 | package node
2 |
3 | import (
4 | "context"
5 | "strconv"
6 | "strings"
7 | "time"
8 |
9 | "github.com/hashicorp/mdns"
10 | "github.com/sirupsen/logrus"
11 | )
12 |
13 | // func main() {
14 | // ip, port, err := queryMDNS()
15 | // if err != nil {
16 | // logrus.Error(err)
17 | // return
18 | //}
19 |
20 | // logrus.Infof("Found %s:%d", ip, port)
21 | //}
22 |
23 | func queryMDNS() (string, string, string) {
24 | entriesCh := make(chan *mdns.ServiceEntry)
25 |
26 | logrus.Info("node: running mdns query")
27 | ctx, cancel := context.WithCancel(context.Background())
28 | go func() {
29 | for {
30 | select {
31 | case <-ctx.Done():
32 | return
33 | default:
34 | // mdns.Lookup("_stampzilla._tcp", entriesCh)
35 | params := mdns.DefaultParams("_stampzilla._tcp")
36 | params.Entries = entriesCh
37 | params.Timeout = time.Second * 5
38 | params.DisableIPv6 = true
39 | err := mdns.Query(params)
40 | if err != nil {
41 | logrus.Error(err)
42 | }
43 | }
44 | }
45 | }()
46 |
47 | var entry *mdns.ServiceEntry
48 | for {
49 | entry = <-entriesCh
50 | if strings.Contains(entry.Name, "_stampzilla._tcp") { // Ignore answers that are not what we are looking for
51 | break
52 | }
53 | }
54 | cancel()
55 | port := strconv.Itoa(entry.Port)
56 | tlsPort := ""
57 | for _, v := range entry.InfoFields {
58 | tmp := strings.SplitN(v, "=", 2)
59 | if len(tmp) != 2 {
60 | continue
61 | }
62 | if tmp[0] == "tlsPort" {
63 | tlsPort = tmp[1]
64 | }
65 | }
66 | logrus.Infof("node: got mdns query response %s:%s (tlsPort=%s)", entry.AddrV4.String(), port, tlsPort)
67 | return entry.AddrV4.String(), port, tlsPort
68 | }
69 |
--------------------------------------------------------------------------------
/nodes/stampzilla-keba-p30/types.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type Report2 struct {
4 | ID string `json:"ID"`
5 | State int `json:"State"`
6 | Error1 int `json:"Error1"`
7 | Error2 int `json:"Error2"`
8 | Plug int `json:"Plug"`
9 | AuthON int `json:"AuthON"`
10 | Authreq int `json:"Authreq"`
11 | EnableSys int `json:"Enable sys"`
12 | EnableUser int `json:"Enable user"`
13 | MaxCurr int `json:"Max curr"`
14 | MaxCurrPercent int `json:"Max curr %"`
15 | CurrHW int `json:"Curr HW"`
16 | CurrUser int `json:"Curr user"`
17 | CurrFS int `json:"Curr FS"`
18 | TmoFS int `json:"Tmo FS"`
19 | CurrTimer int `json:"Curr timer"`
20 | TmoCT int `json:"Tmo CT"`
21 | Setenergy int `json:"Setenergy"`
22 | Output int `json:"Output"`
23 | Input int `json:"Input"`
24 | X2PhaseSwitchSource int `json:"X2 phaseSwitch source"`
25 | X2PhaseSwitch int `json:"X2 phaseSwitch"`
26 | Serial string `json:"Serial"`
27 | Sec int `json:"Sec"`
28 | }
29 | type Report3 struct {
30 | ID string `json:"ID"`
31 | U1 int `json:"U1"`
32 | U2 int `json:"U2"`
33 | U3 int `json:"U3"`
34 | I1 int `json:"I1"`
35 | I2 int `json:"I2"`
36 | I3 int `json:"I3"`
37 | P int `json:"P"`
38 | PF int `json:"PF"`
39 | EPres int `json:"E pres"`
40 | ETotal int `json:"E total"`
41 | Serial string `json:"Serial"`
42 | Sec int `json:"Sec"`
43 | }
44 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/file/file.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "time"
8 | )
9 |
10 | type FileSender struct {
11 | Append bool `json:"append"`
12 | Timestamp bool `json:"timestamp"`
13 | }
14 |
15 | func New(parameters json.RawMessage) *FileSender {
16 | f := &FileSender{}
17 |
18 | json.Unmarshal(parameters, f)
19 |
20 | return f
21 | }
22 |
23 | func (f *FileSender) Trigger(dest []string, body string) error {
24 | var failure error
25 | for _, d := range dest {
26 | err := f.notify(true, d, body)
27 | if err != nil {
28 | failure = err
29 | }
30 | }
31 |
32 | return failure
33 | }
34 |
35 | func (f *FileSender) Release(dest []string, body string) error {
36 | var failure error
37 | for _, d := range dest {
38 | err := f.notify(false, d, body)
39 | if err != nil {
40 | failure = err
41 | }
42 | }
43 |
44 | return failure
45 | }
46 |
47 | func (f *FileSender) notify(trigger bool, filename string, body string) error {
48 | mode := os.O_CREATE | os.O_WRONLY
49 | if f.Append {
50 | mode = os.O_APPEND | os.O_CREATE | os.O_WRONLY
51 | }
52 |
53 | tf, err := os.OpenFile(filename, mode, 0644)
54 | if err != nil {
55 | return err
56 | }
57 |
58 | defer tf.Close()
59 |
60 | line := body
61 | if f.Timestamp {
62 | line = fmt.Sprintf("%s\t%s", time.Now().Format("2006-01-02 15:04:05"), line)
63 | }
64 |
65 | event := "Triggered"
66 | if !trigger {
67 | event = "Released"
68 | }
69 | line = fmt.Sprintf("%s\t%s\r\n", line, event)
70 |
71 | _, err = tf.WriteString(line)
72 | return err
73 | }
74 |
75 | func (f *FileSender) Destinations() (map[string]string, error) {
76 | return nil, fmt.Errorf("not implemented")
77 | }
78 |
--------------------------------------------------------------------------------
/nodes/stampzilla-telldus/main.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include "_cgo_export.h"
6 |
7 | int callbackDeviceEvent = 0;
8 | int callbackDeviceChangeEvent = 0;
9 | int callbackRawDeviceEvent = 0;
10 | int callbackSensorEvent = 0;
11 |
12 | void registerCallbacks() {
13 | tdInit();
14 |
15 | callbackSensorEvent = tdRegisterSensorEvent( (TDSensorEvent)&sensorEvent, 0 );
16 | callbackDeviceEvent = tdRegisterDeviceEvent( (TDDeviceEvent)&deviceEvent, 0 );
17 | callbackDeviceChangeEvent = tdRegisterDeviceChangeEvent( (TDDeviceChangeEvent)&deviceChangeEvent, 0 );
18 | callbackRawDeviceEvent = tdRegisterRawDeviceEvent( (TDRawDeviceEvent)&rawDeviceEvent, 0 );
19 | }
20 |
21 | void unregisterCallbacks() {
22 | tdUnregisterCallback( callbackSensorEvent );
23 | tdUnregisterCallback( callbackDeviceEvent);
24 | tdUnregisterCallback( callbackDeviceChangeEvent );
25 | tdUnregisterCallback( callbackRawDeviceEvent );
26 | tdClose();
27 | }
28 |
29 | int updateDevices() {
30 | int intNumberOfDevices = tdGetNumberOfDevices();
31 | int i;
32 |
33 | for (i = 0; i < intNumberOfDevices; i++) {
34 | int id = tdGetDeviceId( i );
35 | char *name = tdGetName( id );
36 | int methods = tdMethods(id, TELLSTICK_TURNON | TELLSTICK_TURNOFF | TELLSTICK_BELL | TELLSTICK_TOGGLE | TELLSTICK_DIM | TELLSTICK_EXECUTE | TELLSTICK_UP | TELLSTICK_DOWN | TELLSTICK_STOP );
37 |
38 | int state = tdLastSentCommand( id, TELLSTICK_TURNON | TELLSTICK_TURNOFF | TELLSTICK_DIM );
39 | char *value = tdLastSentValue( id );
40 |
41 | newDevice(id,name,methods,state,value);
42 |
43 | tdReleaseString(name);
44 | }
45 |
46 | return intNumberOfDevices;
47 | }
48 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/destinations.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 | import { v4 as makeUUID } from 'uuid';
4 |
5 | const c = defineAction('destinations', [
6 | 'ADD',
7 | 'SAVE',
8 | 'UPDATE',
9 | 'UPDATE_STATE',
10 | ]);
11 |
12 | const defaultState = Map({
13 | list: Map(),
14 | });
15 |
16 | // Actions
17 | export function add(destination) {
18 | return { type: c.ADD, destination };
19 | }
20 | export function save(destination) {
21 | return { type: c.SAVE, destination };
22 | }
23 | export function update(destinations) {
24 | return { type: c.UPDATE, destinations };
25 | }
26 | export function updateState(destinations) {
27 | return { type: c.UPDATE_STATE, destinations };
28 | }
29 |
30 | // Subscribe to channels and register the action for the packages
31 | export function subscribe(dispatch) {
32 | return {
33 | destinations: destinations => dispatch(update(destinations)),
34 | };
35 | }
36 |
37 | // Reducer
38 | export default function reducer(state = defaultState, action) {
39 | switch (action.type) {
40 | case c.ADD: {
41 | const destination = {
42 | ...action.destination,
43 | uuid: makeUUID(),
44 | };
45 | return state.setIn(['list', destination.uuid], fromJS(destination));
46 | }
47 | case c.SAVE: {
48 | return state.mergeIn(
49 | ['list', action.destination.uuid],
50 | fromJS(action.destination),
51 | );
52 | }
53 | case c.UPDATE: {
54 | return state.set('list', fromJS(action.destinations));
55 | }
56 | case c.UPDATE_STATE: {
57 | return state.set('state', fromJS(action.destinations));
58 | }
59 | default:
60 | return state;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/nodes/stampzilla-magicmirror/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
23 | stampzilla magic mirror
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/email/email_test.go:
--------------------------------------------------------------------------------
1 | package email
2 |
3 | import (
4 | "encoding/json"
5 | "net/smtp"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestNew(t *testing.T) {
12 | sender := New(json.RawMessage("{\"server\": \"server1\", \"port\": 123, \"from\": \"from1\", \"password\": \"pass1\"}"))
13 |
14 | assert.Equal(t, "server1", sender.Server)
15 | assert.Equal(t, 123, sender.Port)
16 | assert.Equal(t, "from1", sender.From)
17 | assert.Equal(t, "pass1", sender.Password)
18 | }
19 |
20 | func TestTrigger(t *testing.T) {
21 | f, r := mockSend(nil)
22 | sender := &EmailSender{send: f}
23 | err := sender.Trigger([]string{"me@example.com"}, "Hello World")
24 |
25 | assert.NoError(t, err)
26 | assert.Equal(t, "From: \nSubject: stampzilla - Triggered\n\nHello World", string(r.msg))
27 | }
28 |
29 | func TestRelease(t *testing.T) {
30 | f, r := mockSend(nil)
31 | sender := &EmailSender{send: f}
32 | err := sender.Release([]string{"me@example.com"}, "Hello World")
33 |
34 | assert.NoError(t, err)
35 | assert.Equal(t, "From: \nSubject: stampzilla - Released\n\nHello World", string(r.msg))
36 | }
37 |
38 | func TestDestinations(t *testing.T) {
39 | sender := &EmailSender{}
40 | d, err := sender.Destinations()
41 | assert.Nil(t, d)
42 | assert.Error(t, err)
43 | }
44 |
45 | func mockSend(errToReturn error) (func(string, smtp.Auth, string, []string, []byte) error, *emailRecorder) {
46 | r := new(emailRecorder)
47 | return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
48 | *r = emailRecorder{addr, a, from, to, msg}
49 | return errToReturn
50 | }, r
51 | }
52 |
53 | type emailRecorder struct {
54 | addr string
55 | auth smtp.Auth
56 | from string
57 | to []string
58 | msg []byte
59 | }
60 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/sender_test.go:
--------------------------------------------------------------------------------
1 | package notification
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestCreateFileSender(t *testing.T) {
14 | s := &Sender{
15 | Type: "file",
16 | }
17 |
18 | d1 := &Destination{
19 | Name: "name",
20 | UUID: "uuid",
21 | }
22 |
23 | err := s.Trigger(d1, "test")
24 | assert.NoError(t, err)
25 |
26 | err = s.Release(d1, "test")
27 | assert.NoError(t, err)
28 |
29 | d, err := s.Destinations()
30 | assert.Error(t, err)
31 | assert.Nil(t, d)
32 | }
33 |
34 | func TestCreateUnknownSender(t *testing.T) {
35 | s := &Sender{
36 | Type: "unknown",
37 | }
38 |
39 | d1 := &Destination{
40 | Name: "name",
41 | UUID: "uuid",
42 | }
43 |
44 | err := s.Trigger(d1, "test")
45 | assert.Error(t, err)
46 |
47 | err = s.Release(d1, "test")
48 | assert.Error(t, err)
49 |
50 | d, err := s.Destinations()
51 | assert.Error(t, err)
52 | assert.Nil(t, d)
53 | }
54 |
55 | func TestReadWriteSenders(t *testing.T) {
56 | file, err := ioutil.TempFile("", "readwritesenders")
57 | if err != nil {
58 | log.Fatal(err)
59 | }
60 | defer os.Remove(file.Name())
61 |
62 | s1 := NewSenders()
63 | s2 := NewSenders()
64 |
65 | sender1 := Sender{
66 | Name: "name1",
67 | UUID: "uuid1",
68 | Type: "type1",
69 | Parameters: json.RawMessage("null"),
70 | }
71 | s1.Add(sender1)
72 |
73 | err = s1.Save(file.Name())
74 | assert.NoError(t, err)
75 |
76 | err = s2.Load(file.Name())
77 | assert.NoError(t, err)
78 |
79 | sender2, ok := s2.Get("uuid1")
80 | assert.True(t, ok)
81 | assert.Equal(t, sender1, sender2)
82 |
83 | assert.Len(t, s2.All(), 1)
84 |
85 | s2.Remove("uuid1")
86 | assert.Len(t, s2.All(), 0)
87 | }
88 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, compose, createStore } from 'redux';
2 | import Immutable from 'immutable';
3 | import ReduxThunk from 'redux-thunk';
4 | import persistState from 'redux-localstorage';
5 |
6 | import destinations from './middlewares/destinations';
7 | import persons from './middlewares/persons';
8 | import rootReducer from './ducks';
9 | import rules from './middlewares/rules';
10 | import savedstates from './middlewares/savedstates';
11 | import schedules from './middlewares/schedules';
12 | import senders from './middlewares/senders';
13 | import toast from './middlewares/toast';
14 |
15 | const middleware = [
16 | toast,
17 | ReduxThunk,
18 | rules,
19 | persons,
20 | destinations,
21 | senders,
22 | schedules,
23 | savedstates,
24 | ];
25 |
26 | const preloadedState = undefined;
27 |
28 | const composeEnhancers = (typeof window !== 'undefined'
29 | && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) // eslint-disable-line no-underscore-dangle
30 | || compose;
31 |
32 | const localStorageConfig = {
33 | slicer: (paths) => (state) => (paths ? state.filter((v, k) => paths.indexOf(k) > -1) : state),
34 | serialize: (subset) => JSON.stringify(subset.toJS()),
35 | deserialize: (serializedData) => Immutable.fromJS(JSON.parse(serializedData)) || Immutable.Map(),
36 | merge: (initialState, persistedState) => (initialState ? initialState.mergeDeep(persistedState) : persistedState),
37 | };
38 |
39 | const newStore = (initialState, extraMiddlewares = []) => createStore(
40 | rootReducer,
41 | initialState,
42 | composeEnhancers(
43 | applyMiddleware(...extraMiddlewares, ...middleware),
44 | persistState(['app'], localStorageConfig),
45 | ),
46 | );
47 | const store = newStore(preloadedState);
48 |
49 | export default store;
50 | export { newStore };
51 |
--------------------------------------------------------------------------------
/nodes/stampzilla-modbus/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "device":"/dev/ttyUSB0",
4 | "registers": {
5 | "outsideTemp": {
6 | "Name": "outSideTemp",
7 | "Id": 218,
8 | "Base": 10
9 | },
10 | "supplyAirTemp": {
11 | "Name": "supplyTemp",
12 | "Id": 214,
13 | "Base": 10
14 | },
15 | "exitAirTemp": {
16 | "Name": "exitAirTemp",
17 | "Id": 215,
18 | "Base": 10
19 | },
20 | "coolerSignal": {
21 | "Name": "coolerSignal",
22 | "Id": 204
23 | },
24 | "bypassSignal": {
25 | "Name": "bypassSignal",
26 | "Id": 301
27 | },
28 | "fanSpeed": {
29 | "Name": "fanSpeed",
30 | "Id": 101
31 | },
32 | "defrostState": {
33 | "Name": "defrostState",
34 | "Id": 651
35 | },
36 | "defrostConfig": {
37 | "Name": "defrostConfig",
38 | "Id": 652
39 | },
40 | "defrostMode": {
41 | "Name": "defrostMode",
42 | "Id": 654
43 | },
44 | "supplyFanRpm": {
45 | "Name": "supplyFanRpm",
46 | "Id": 111
47 | },
48 | "exitFanRpm": {
49 | "Name": "exitFanRpm",
50 | "Id": 112
51 | },
52 | "supplyFanPwm": {
53 | "Name": "supplyFanPwm",
54 | "Id": 109
55 | },
56 | "exitFanPwm": {
57 | "Name": "exitFanPwm",
58 | "Id": 110
59 | },
60 | "preHeaterRelays": {
61 | "Name": "preHeaterRelays",
62 | "Id": 751
63 | },
64 | "alarms": {
65 | "Name": "alarms",
66 | "Id": 801
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/rules.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 | import { v4 as makeUUID } from 'uuid';
4 |
5 | const c = defineAction('rules', [
6 | 'ADD',
7 | 'SAVE',
8 | 'REMOVE',
9 | 'UPDATE',
10 | 'UPDATE_STATE',
11 | ]);
12 |
13 | const defaultState = Map({
14 | list: Map(),
15 | state: Map(),
16 | });
17 |
18 | // Actions
19 | export function add(rule) {
20 | return { type: c.ADD, rule };
21 | }
22 | export function save(rule) {
23 | return { type: c.SAVE, rule };
24 | }
25 | export function remove(uuid) {
26 | return { type: c.REMOVE, uuid };
27 | }
28 | export function update(rules) {
29 | return { type: c.UPDATE, rules };
30 | }
31 | export function updateState(rules) {
32 | return { type: c.UPDATE_STATE, rules };
33 | }
34 |
35 | // Subscribe to channels and register the action for the packages
36 | export function subscribe(dispatch) {
37 | return {
38 | rules: (rules) => dispatch(update(rules)),
39 | server: ({ rules }) => rules && dispatch(updateState(rules)),
40 | };
41 | }
42 |
43 | // Reducer
44 | export default function reducer(state = defaultState, action) {
45 | switch (action.type) {
46 | case c.ADD: {
47 | const rule = {
48 | ...action.rule,
49 | uuid: makeUUID(),
50 | };
51 | return state.setIn(['list', rule.uuid], fromJS(rule));
52 | }
53 | case c.SAVE: {
54 | return state.mergeIn(['list', action.rule.uuid], fromJS(action.rule));
55 | }
56 | case c.REMOVE: {
57 | return state.deleteIn(['list', action.uuid]);
58 | }
59 | case c.UPDATE: {
60 | return state.set('list', fromJS(action.rules));
61 | }
62 | case c.UPDATE_STATE: {
63 | return state.set('state', fromJS(action.rules));
64 | }
65 | default:
66 | return state;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/Link.js:
--------------------------------------------------------------------------------
1 | import { Link as RouterLink } from 'react-router-dom';
2 | import { withRouter } from 'react-router';
3 | import React from 'react';
4 | import Url from 'url';
5 | import classnames from 'classnames';
6 |
7 | class Link extends React.Component {
8 | constructor() {
9 | super();
10 | this.state = {
11 | isLocal: false,
12 | };
13 | }
14 |
15 | /* eslint-disable react/no-did-mount-set-state */
16 | componentDidMount() {
17 | const { to } = this.props;
18 | if (to && typeof window !== 'undefined') {
19 | const url = Url.parse(to);
20 | if (
21 | window.location.hostname === url.hostname
22 | || !url.hostname
23 | || !url.hostname.length
24 | ) {
25 | this.setState({
26 | isLocal: true,
27 | localTo: to.replace('www.', '').replace(window.location.origin, ''),
28 | });
29 | }
30 | }
31 | }
32 | /* eslint-enable */
33 |
34 | render() {
35 | const {
36 | to,
37 | className,
38 | children,
39 | location,
40 | activeClass,
41 | onClick,
42 | exact,
43 | } = this.props;
44 | const { isLocal, localTo } = this.state;
45 | const active = localTo
46 | && activeClass
47 | && ((location.pathname.substring(0, localTo.length) === localTo && !exact)
48 | || (location.pathname === localTo && exact))
49 | ? activeClass
50 | : null;
51 |
52 | return isLocal ? (
53 |
58 | {children}
59 |
60 | ) : (
61 |
62 | {children}
63 |
64 | );
65 | }
66 | }
67 |
68 | export default withRouter(Link);
69 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/websocket/websocket.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "github.com/lesismal/melody"
5 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models"
6 | )
7 |
8 | type SessionKey string
9 |
10 | const (
11 | KeyProtocol SessionKey = "protocol"
12 | KeyID SessionKey = "ID"
13 | )
14 |
15 | func (sk SessionKey) String() string {
16 | return string(sk)
17 | }
18 |
19 | type Sender interface {
20 | SendToID(to string, msgType string, data interface{}) error
21 | SendToProtocol(to string, msgType string, data interface{}) error
22 | BroadcastWithFilter(msgType string, data interface{}, fn func(*melody.Session) bool) error
23 | }
24 |
25 | type sender struct {
26 | Melody *melody.Melody
27 | }
28 |
29 | func NewWebsocketSender(m *melody.Melody) Sender {
30 | return &sender{
31 | Melody: m,
32 | }
33 | }
34 |
35 | func (ws *sender) sendMessageTo(key SessionKey, to string, msg *models.Message) error {
36 | return msg.WriteWithFilter(ws.Melody, func(s *melody.Session) bool {
37 | v, exists := s.Get(key.String())
38 | return exists && v == to
39 | })
40 | }
41 |
42 | func (ws *sender) SendToID(to string, msgType string, data interface{}) error {
43 | message, err := models.NewMessage(msgType, data)
44 | if err != nil {
45 | return err
46 | }
47 | return ws.sendMessageTo(KeyID, to, message)
48 | }
49 |
50 | func (ws *sender) SendToProtocol(to string, msgType string, data interface{}) error {
51 | message, err := models.NewMessage(msgType, data)
52 | if err != nil {
53 | return err
54 | }
55 | return ws.sendMessageTo(KeyProtocol, to, message)
56 | }
57 |
58 | func (ws *sender) BroadcastWithFilter(msgType string, data interface{}, fn func(*melody.Session) bool) error {
59 | message, err := models.NewMessage(msgType, data)
60 | if err != nil {
61 | return err
62 | }
63 | return message.WriteWithFilter(ws.Melody, fn)
64 | }
65 |
--------------------------------------------------------------------------------
/nodes/stampzilla-spc/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/e2e"
9 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestUpdateStateFromUDP(t *testing.T) {
14 | // logrus.SetLevel(logrus.DebugLevel)
15 | main, _, cleanup := e2e.SetupWebsocketTest(t)
16 | defer cleanup()
17 | e2e.AcceptCertificateRequest(t, main)
18 |
19 | config.EDPPort = "9999"
20 | _, node, listenPort := start()
21 | listenPort <- config.EDPPort
22 |
23 | time.Sleep(time.Millisecond * 100) // Wait for udp server to start
24 | err := writeUDP(config.EDPPort)
25 | assert.NoError(t, err)
26 |
27 | e2e.WaitFor(t, 1*time.Second, "we should have 1 device", func() bool {
28 | return len(main.Store.GetDevices().All()) == 1
29 | })
30 | // spew.Dump(main.Store.Devices.All())
31 | // spew.Dump(node.Devices.All())
32 |
33 | // Assert that the device exists in the server after we got UDP packet
34 | assert.Equal(t, "Zone Kök IR", main.Store.GetDevices().Get(devices.ID{ID: "zone.8", Node: node.UUID}).Name)
35 | assert.Equal(t, "Zone Kök IR", main.Store.GetDevices().Get(devices.ID{ID: "zone.8", Node: node.UUID}).Name)
36 | }
37 |
38 | func writeUDP(port string) error {
39 | d := []byte{0x45, 0x2, 0x0, 0x3e, 0x0, 0x0, 0x0, 0xe8, 0x3, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x95, 0xa1, 0x33, 0x0, 0x45, 0x32, 0x5b, 0x23, 0x31, 0x30, 0x30, 0x30, 0x7c, 0x32, 0x31, 0x31, 0x35, 0x35, 0x37, 0x30, 0x33, 0x31, 0x31, 0x32, 0x30, 0x32, 0x30, 0x7c, 0x5a, 0x4f, 0x7c, 0x38, 0x7c, 0x4b, 0xf6, 0x6b, 0x20, 0x49, 0x52, 0xa6, 0x5a, 0x4f, 0x4e, 0x45, 0xa6, 0x31, 0xa6, 0x4c, 0x61, 0x72, 0x6d, 0x7c, 0x7c, 0x30, 0x5d}
40 | conn, err := net.Dial("udp", "127.0.0.1:"+port)
41 | if err != nil {
42 | return err
43 | }
44 | _, err = conn.Write(d)
45 | return err
46 | }
47 |
--------------------------------------------------------------------------------
/nodes/stampzilla-chromecast/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/url"
7 | "time"
8 |
9 | "github.com/sirupsen/logrus"
10 | "github.com/stampzilla/gocast/discovery"
11 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
12 | "github.com/stampzilla/stampzilla-go/v2/pkg/node"
13 | )
14 |
15 | var chromecasts = &State{
16 | Chromecasts: make(map[string]*Chromecast),
17 | }
18 |
19 | func main() {
20 | node := node.New("chromecast")
21 |
22 | // node.OnConfig(updatedConfig)
23 |
24 | ctx, cancel := context.WithCancel(context.Background())
25 | node.OnShutdown(func() {
26 | cancel()
27 | })
28 | node.OnRequestStateChange(stateChange)
29 |
30 | if err := node.Connect(); err != nil {
31 | logrus.Error(err)
32 | return
33 | }
34 |
35 | discovery := discovery.NewService()
36 | go discoveryListner(ctx, node, discovery)
37 | discovery.Start(ctx, time.Second*10)
38 |
39 | node.Wait()
40 | }
41 | func stateChange(state devices.State, device *devices.Device) error {
42 | var err error
43 | state.String("say", func(text string) {
44 | cc := chromecasts.GetByUUID(device.ID.ID)
45 | if cc != nil {
46 | base := "https://translate.google.com/translate_tts?client=tw-ob&ie=UTF-8&q=%s&tl=%s"
47 | u := fmt.Sprintf(base, url.QueryEscape(text), url.QueryEscape("sv"))
48 | cc.PlayURL(u, "audio/mpeg")
49 | }
50 | })
51 | if err != nil {
52 | return err
53 | }
54 |
55 | return err
56 | }
57 |
58 | func discoveryListner(ctx context.Context, node *node.Node, discovery *discovery.Service) {
59 | for {
60 | select {
61 | case device := <-discovery.Found():
62 | logrus.Debugf("New device discovered: %s", device.String())
63 | d := NewChromecast(node, device)
64 | chromecasts.Add(d)
65 | err := device.Connect(ctx)
66 | if err != nil {
67 | logrus.Error(err)
68 | }
69 | case <-ctx.Done():
70 | return
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/destinations.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/sirupsen/logrus"
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/notification"
8 | )
9 |
10 | func (store *Store) GetDestinations() map[string]*notification.Destination {
11 | store.RLock()
12 | defer store.RUnlock()
13 | return store.Destinations.All()
14 | }
15 |
16 | func (store *Store) AddOrUpdateDestination(dest *notification.Destination) {
17 | if dest == nil {
18 | return
19 | }
20 |
21 | oldDest := store.Destinations.Get(dest.UUID)
22 | if oldDest != nil && oldDest.Equal(dest) {
23 | return
24 | }
25 |
26 | store.Destinations.Add(dest)
27 | err := store.Destinations.Save("destinations.json")
28 | if err != nil {
29 | logrus.Error(err)
30 | return
31 | }
32 | store.runCallbacks("destinations")
33 | }
34 |
35 | func (store *Store) TriggerDestination(dest string, body string) error {
36 | destination := store.Destinations.Get(dest)
37 | if destination == nil {
38 | return fmt.Errorf("destination definition not found")
39 | }
40 |
41 | sender, ok := store.Senders.Get(destination.Sender)
42 | if !ok {
43 | return fmt.Errorf("sender not found")
44 | }
45 |
46 | return sender.Trigger(destination, body)
47 | }
48 |
49 | func (store *Store) ReleaseDestination(dest string, body string) error {
50 | destination := store.Destinations.Get(dest)
51 | if destination == nil {
52 | return fmt.Errorf("destination definition not found")
53 | }
54 |
55 | sender, ok := store.Senders.Get(destination.Sender)
56 | if !ok {
57 | return fmt.Errorf("sender not found")
58 | }
59 |
60 | return sender.Release(destination, body)
61 | }
62 |
63 | func (store *Store) GetSenderDestinations(id string) (map[string]string, error) {
64 | sender, ok := store.Senders.Get(id)
65 | if !ok {
66 | return nil, fmt.Errorf("sender not found")
67 | }
68 |
69 | return sender.Destinations()
70 | }
71 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/routes/automation/components/SavedStatePicker.scss:
--------------------------------------------------------------------------------
1 | .saved-state-picker {
2 | background: #f0f0f0;
3 |
4 | font-weight: 400;
5 | color: #212529;
6 | vertical-align: middle;
7 | user-select: none;
8 | border: 1px solid transparent;
9 | padding: 0.375rem 0.75rem;
10 | font-size: 1rem;
11 | line-height: 1.5;
12 | border-radius: 0.25rem;
13 | }
14 |
15 | .saved-state-modal {
16 | .modal-content {
17 | border-top: none rgb(222, 226, 230) !important;
18 | border-right-color: rgb(222, 226, 230) !important;
19 | border-bottom-color: rgb(222, 226, 230) !important;
20 | border-left-color: rgb(222, 226, 230) !important;
21 | }
22 | }
23 |
24 |
25 |
26 | :global {
27 | .ReactModal__Overlay {
28 | -webkit-perspective: 600;
29 | perspective: 600;
30 | opacity: 0;
31 | overflow-x: hidden;
32 | overflow-y: scroll !important;
33 | background-color: rgba(0, 0, 0, 0.5);
34 | z-index: 20000;
35 | }
36 |
37 | .ReactModal__Overlay--after-open {
38 | opacity: 1;
39 | transition: opacity 150ms ease-out;
40 | }
41 |
42 | .ReactModal__Content {
43 | -webkit-transform: translateY(30px);
44 | transform: translateY(30px);
45 | z-index: 20000;
46 |
47 | .modal-content{
48 | .nav-link {
49 | cursor:pointer;
50 | outline: none;
51 | }
52 | }
53 | }
54 |
55 | .ReactModal__Content--after-open {
56 | -webkit-transform: translateY(0);
57 | transform: translateY(0);
58 | transition: all 150ms ease-in;
59 | }
60 |
61 | .ReactModal__Overlay--before-close {
62 | opacity: 0;
63 | }
64 |
65 | .ReactModal__Content--before-close {
66 | -webkit-transform: translateY(30px);
67 | transform: translateY(30px);
68 | transition: all 150ms ease-in;
69 | }
70 |
71 | .ReactModal__Content.modal-dialog {
72 | border: none;
73 | background-color: transparent;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/wirepusher/wirepusher_test.go:
--------------------------------------------------------------------------------
1 | package wirepusher
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestNew(t *testing.T) {
13 | sender := New(json.RawMessage("{\"title\": \"title1\", \"type\": \"type1\", \"action\": \"action1\"}"))
14 |
15 | assert.Equal(t, "title1", sender.Title)
16 | assert.Equal(t, "type1", sender.Type)
17 | assert.Equal(t, "action1", sender.Action)
18 | }
19 |
20 | func TestTrigger(t *testing.T) {
21 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
22 | assert.Equal(t, "/send?action=action1&id=deviceId1&message=test+body&title=title1+-+Triggered&type=type1", req.URL.String())
23 | rw.Write([]byte(`OK`))
24 | }))
25 | defer server.Close()
26 |
27 | sender := New(json.RawMessage("{\"title\": \"title1\", \"type\": \"type1\", \"action\": \"action1\"}"))
28 | sender.url = server.URL + "/send"
29 | err := sender.Trigger([]string{"deviceId1"}, "test body")
30 |
31 | assert.NoError(t, err)
32 | }
33 |
34 | func TestRelease(t *testing.T) {
35 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
36 | assert.Equal(t, "/send?action=action1&id=deviceId1&message=test+body&title=title1+-+Released&type=type1", req.URL.String())
37 | rw.Write([]byte(`OK`))
38 | }))
39 | defer server.Close()
40 |
41 | sender := New(json.RawMessage("{\"title\": \"title1\", \"type\": \"type1\", \"action\": \"action1\"}"))
42 | sender.url = server.URL + "/send"
43 | err := sender.Release([]string{"deviceId1"}, "test body")
44 |
45 | assert.NoError(t, err)
46 | }
47 |
48 | func TestDestinations(t *testing.T) {
49 | sender := New(json.RawMessage("{\"title\": \"title1\", \"type\": \"type1\", \"action\": \"action1\"}"))
50 | d, err := sender.Destinations()
51 | assert.Nil(t, d)
52 | assert.Error(t, err)
53 | }
54 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/ducks/schedules.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 | import { defineAction } from 'redux-define';
3 | import { v4 as makeUUID } from 'uuid';
4 |
5 | const c = defineAction(
6 | 'schedules',
7 | ['ADD', 'SAVE', 'REMOVE', 'UPDATE', 'UPDATE_STATE'],
8 | );
9 |
10 | const defaultState = Map({
11 | list: Map(),
12 | state: Map(),
13 | });
14 |
15 | // Actions
16 | export function add(schedule) {
17 | return { type: c.ADD, schedule };
18 | }
19 | export function save(schedule) {
20 | return { type: c.SAVE, schedule };
21 | }
22 | export function remove(uuid) {
23 | return { type: c.REMOVE, uuid };
24 | }
25 | export function update(schedules) {
26 | return { type: c.UPDATE, schedules };
27 | }
28 | export function updateState(schedules) {
29 | return { type: c.UPDATE_STATE, schedules };
30 | }
31 |
32 | // Subscribe to channels and register the action for the packages
33 | export function subscribe(dispatch) {
34 | return {
35 | schedules: (schedules) => dispatch(update(schedules)),
36 | server: ({ schedules }) => schedules && dispatch(updateState(schedules)),
37 | };
38 | }
39 |
40 | // Reducer
41 | export default function reducer(state = defaultState, action) {
42 | switch (action.type) {
43 | case c.ADD: {
44 | const schedule = {
45 | ...action.schedule,
46 | uuid: makeUUID(),
47 | };
48 | return state
49 | .setIn(['list', schedule.uuid], fromJS(schedule));
50 | }
51 | case c.SAVE: {
52 | return state
53 | .mergeIn(['list', action.schedule.uuid], fromJS(action.schedule));
54 | }
55 | case c.REMOVE: {
56 | return state.deleteIn(['list', action.uuid]);
57 | }
58 | case c.UPDATE: {
59 | return state
60 | .set('list', fromJS(action.schedules));
61 | }
62 | case c.UPDATE_STATE: {
63 | return state
64 | .set('state', fromJS(action.schedules));
65 | }
66 | default: return state;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/nodes/stampzilla-exoline/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "interval": "30s",
3 | "host": "192.168.13.57:26486",
4 | "variables": [
5 | {
6 | "name": "ExtractAir",
7 | "loadNumber": 58,
8 | "cell": 585,
9 | "type": "float"
10 | },
11 | {
12 | "name": "OutdoorAir",
13 | "loadNumber": 58,
14 | "cell": 579,
15 | "type": "float"
16 | },
17 | {
18 | "name": "ExhaustAir",
19 | "loadNumber": 58,
20 | "cell": 588,
21 | "type": "float"
22 | },
23 | {
24 | "name": "SupplyAir",
25 | "loadNumber": 58,
26 | "cell": 582,
27 | "type": "float"
28 | },
29 | {
30 | "name": "SetSupplyAir",
31 | "loadNumber": 4,
32 | "cell": 8,
33 | "type": "float",
34 | "write": true
35 | },
36 | {
37 | "name": "HeatingValve",
38 | "loadNumber": 4,
39 | "cell": 60,
40 | "type": "float"
41 | },
42 | {
43 | "name": "CoolingValve",
44 | "loadNumber": 4,
45 | "cell": 63,
46 | "type": "float"
47 | },
48 | {
49 | "name": "Bypass",
50 | "loadNumber": 4,
51 | "cell": 66,
52 | "type": "float"
53 | },
54 | {
55 | "name": "SupplyFan",
56 | "loadNumber": 4,
57 | "cell": 69,
58 | "type": "float"
59 | },
60 | {
61 | "name": "ExtractFan",
62 | "loadNumber": 4,
63 | "cell": 72,
64 | "type": "float"
65 | },
66 | {
67 | "name": "FanMode",
68 | "loadNumber": 4,
69 | "cell": 42,
70 | "type": "int",
71 | "write": true
72 | }
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/nodes/stampzilla-knx/setup.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
5 | "github.com/stampzilla/stampzilla-go/v2/pkg/node"
6 | )
7 |
8 | func setupLight(node *node.Node, tunnel *tunnel, light light) {
9 | traits := []string{}
10 |
11 | if light.ControlSwitch != "" {
12 | traits = append(traits, "OnOff")
13 | }
14 | if light.ControlBrightness != "" {
15 | traits = append(traits, "Brightness")
16 | }
17 |
18 | dev := &devices.Device{
19 | Name: light.ID,
20 | ID: devices.ID{ID: "light." + light.ID},
21 | Traits: traits,
22 | State: devices.State{
23 | "on": false,
24 | },
25 | }
26 | node.AddOrUpdate(dev)
27 |
28 | if light.StateSwitch != "" {
29 | tunnel.AddLink(light.StateSwitch, "on", "bool", dev)
30 | }
31 |
32 | if light.StateBrightness != "" {
33 | tunnel.AddLink(light.StateBrightness, "brightness", "procentage", dev)
34 | }
35 | }
36 |
37 | func setupSensor(node *node.Node, tunnel *tunnel, sensor sensor) {
38 | dev := &devices.Device{
39 | Name: sensor.ID,
40 | ID: devices.ID{ID: "sensor." + sensor.ID},
41 | State: make(devices.State),
42 | }
43 | node.AddOrUpdate(dev)
44 |
45 | if sensor.Temperature != "" {
46 | tunnel.AddLink(sensor.Temperature, "temperature", "temperature", dev)
47 | }
48 | if sensor.Motion != "" {
49 | tunnel.AddLink(sensor.Motion, "motion", "bool", dev)
50 | }
51 | if sensor.MotionTrue != "" {
52 | tunnel.AddLink(sensor.Motion, "motionTrue", "bool", dev)
53 | }
54 | if sensor.Lux != "" {
55 | tunnel.AddLink(sensor.Lux, "lux", "lux", dev)
56 | }
57 | if sensor.Humidity != "" {
58 | tunnel.AddLink(sensor.Humidity, "humidity", "humidity", dev)
59 | }
60 | if sensor.Co2 != "" {
61 | tunnel.AddLink(sensor.Co2, "co2", "co2", dev)
62 | }
63 | if sensor.Voc != "" {
64 | tunnel.AddLink(sensor.Voc, "voc", "voc", dev)
65 | }
66 | if sensor.DewPoint != "" {
67 | tunnel.AddLink(sensor.DewPoint, "dewpoint", "dewPoint", dev)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/store/request.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | type Request struct {
4 | Identity string `json:"identity"`
5 | Subject RequestSubject `json:"subject"`
6 | Connection string `json:"connection"`
7 | Type string `json:"type"`
8 | Version string `json:"version"`
9 |
10 | Approved chan bool `json:"-"`
11 | }
12 |
13 | type RequestSubject struct {
14 | CommonName string `json:"common_name,omitempty"`
15 | SerialNumber string `json:"serial_number,omitempty"`
16 | Country []string `json:"country,omitempty"`
17 | Organization []string `json:"organization,omitempty"`
18 | OrganizationalUnit []string `json:"organizational_unit,omitempty"`
19 | Locality []string `json:"locality,omitempty"`
20 | Province []string `json:"province,omitempty"`
21 | StreetAddress []string `json:"street_address,omitempty"`
22 | PostalCode []string `json:"postal_code,omitempty"`
23 | Names []interface{} `json:"names,omitempty"`
24 | // ExtraNames []interface{} `json:"extra_names,omitempty"`
25 | }
26 |
27 | func (store *Store) GetRequests() []Request {
28 | store.RLock()
29 | defer store.RUnlock()
30 | return store.Requests
31 | }
32 |
33 | func (store *Store) AddRequest(r Request) {
34 | store.Lock()
35 | store.Requests = append(store.Requests, r)
36 | store.Unlock()
37 |
38 | store.runCallbacks("requests")
39 | }
40 |
41 | func (store *Store) RemoveRequest(c string, approved bool) {
42 | store.Lock()
43 | for i, r := range store.Requests {
44 | if r.Connection == c {
45 | if r.Approved != nil {
46 | if approved {
47 | r.Approved <- true
48 | }
49 | close(r.Approved)
50 | r.Approved = nil
51 | }
52 | store.Requests = append(store.Requests[:i], store.Requests[i+1:]...)
53 | }
54 | }
55 | store.Unlock()
56 |
57 | store.runCallbacks("requests")
58 | }
59 |
60 | func (store *Store) AcceptRequest(c string) {
61 | store.RemoveRequest(c, true)
62 | }
63 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/wirepusher/wirepusher.go:
--------------------------------------------------------------------------------
1 | package wirepusher
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "net/url"
8 | )
9 |
10 | type WirePusherSender struct {
11 | Title string `json:"title"`
12 | Type string `json:"type"`
13 | Action string `json:"action"`
14 | url string
15 | }
16 |
17 | func New(parameters json.RawMessage) *WirePusherSender {
18 | wp := &WirePusherSender{url: "https://wirepusher.com/send"}
19 |
20 | json.Unmarshal(parameters, wp)
21 |
22 | return wp
23 | }
24 |
25 | func (wp *WirePusherSender) Trigger(dest []string, body string) error {
26 | var failure error
27 | for _, d := range dest {
28 | err := wp.notify(true, d, body)
29 | if err != nil {
30 | failure = err
31 | }
32 | }
33 |
34 | return failure
35 | }
36 |
37 | func (wp *WirePusherSender) Release(dest []string, body string) error {
38 | var failure error
39 | for _, d := range dest {
40 | err := wp.notify(false, d, body)
41 | if err != nil {
42 | failure = err
43 | }
44 | }
45 |
46 | return failure
47 | }
48 |
49 | func (wp *WirePusherSender) notify(trigger bool, dest string, body string) error {
50 | u, err := url.Parse(wp.url)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | event := "Triggered"
56 | if !trigger {
57 | event = "Released"
58 | }
59 |
60 | q := u.Query()
61 |
62 | q.Set("id", dest)
63 | q.Set("title", fmt.Sprintf("%s - %s", wp.Title, event))
64 | q.Set("message", body)
65 | q.Set("type", wp.Type)
66 | q.Set("action", wp.Action)
67 |
68 | u.RawQuery = q.Encode()
69 |
70 | req, err := http.NewRequest("GET", u.String(), nil)
71 | if err != nil {
72 | return err
73 | }
74 |
75 | client := &http.Client{}
76 | resp, err := client.Do(req)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | defer resp.Body.Close()
82 |
83 | // b, err := ioutil.ReadAll(resp.Body)
84 | // spew.Dump(b)
85 |
86 | return err
87 | }
88 |
89 | func (wp *WirePusherSender) Destinations() (map[string]string, error) {
90 | return nil, fmt.Errorf("not implemented")
91 | }
92 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/routes/dashboard/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import Card from '../../components/Card';
5 | import Device from './Device';
6 |
7 | class Dashboard extends Component {
8 | render() {
9 | const { devices, nodes } = this.props;
10 |
11 | const devicesByNode = devices.reduce((acc, device) => {
12 | const [node] = device.get('id').split('.', 2);
13 | if (acc[node] === undefined) {
14 | acc[node] = [];
15 | }
16 | acc[node].push(device);
17 | return acc;
18 | }, {});
19 |
20 | return (
21 |
22 |
25 | {Object.keys(devicesByNode).map(nodeId => (
26 |
35 |
43 | {devicesByNode[nodeId]
44 | .sort((a, b) => a.get('name').localeCompare(b.get('name')))
45 | .map(device => (
46 |
50 | ))}
51 |
52 |
53 | ))}
54 |
55 |
56 | );
57 | }
58 | }
59 |
60 | const mapToProps = state => ({
61 | devices: state.getIn(['devices', 'list']),
62 | nodes: state.getIn(['nodes', 'list']),
63 | });
64 |
65 | export default connect(mapToProps)(Dashboard);
66 |
--------------------------------------------------------------------------------
/nodes/stampzilla-streamdeck/keys.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "image/color"
7 | "strconv"
8 |
9 | "github.com/llgcode/draw2d"
10 | "github.com/llgcode/draw2d/draw2dimg"
11 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-streamdeck/streamdeck"
12 | )
13 |
14 | func drawTempToKey(deck *streamdeck.StreamDeck, label string, value float32, key int) {
15 | dest := image.NewRGBA(image.Rect(0, 0, ICON_SIZE, ICON_SIZE))
16 | gc := draw2dimg.NewGraphicContext(dest)
17 |
18 | gc.SetFillColor(color.RGBA{0xff, 0x66, 0x00, 0xff})
19 | gc.SetStrokeColor(color.RGBA{0xff, 0xff, 0xff, 0xff})
20 |
21 | gc.SetFontSize(28)
22 | gc.SetFontData(draw2d.FontData{
23 | Name: "Roboto",
24 | })
25 |
26 | text := fmt.Sprintf("%.0f", value)
27 | left, top, right, bottom := gc.GetStringBounds(text)
28 | gc.FillStringAt(text, 72/2-((right-left)/2), 72/2)
29 |
30 | // Label
31 | gc.SetFillColor(color.RGBA{0xff, 0xff, 0xff, 0xff})
32 | gc.SetFontSize(14)
33 | left, top, right, bottom = gc.GetStringBounds(label)
34 | gc.FillStringAt(label, 72/2-((right-left)/2), 72-((bottom-top)/2))
35 |
36 | deck.WriteImageToKey(dest, key)
37 | }
38 |
39 | func drawStateToKey(deck *streamdeck.StreamDeck, label string, value interface{}, key int) {
40 | dest := image.NewRGBA(image.Rect(0, 0, ICON_SIZE, ICON_SIZE))
41 | gc := draw2dimg.NewGraphicContext(dest)
42 |
43 | text := ""
44 | switch state := value.(type) {
45 | case bool:
46 | text = strconv.FormatBool(state)
47 | case int:
48 | text = "int"
49 | default:
50 | text = "unkn"
51 | }
52 |
53 | // State
54 | gc.SetFillColor(color.RGBA{0xff, 0xff, 0xff, 0xff})
55 | gc.SetStrokeColor(color.RGBA{0xff, 0xff, 0xff, 0xff})
56 |
57 | gc.SetFontSize(28)
58 | gc.SetFontData(draw2d.FontData{
59 | Name: "Roboto",
60 | })
61 |
62 | left, top, right, bottom := gc.GetStringBounds(text)
63 | gc.FillStringAt(text, 72/2-((right-left)/2), 72/2)
64 |
65 | // Label
66 | gc.SetFillColor(color.RGBA{0xff, 0xff, 0xff, 0xff})
67 | gc.SetFontSize(14)
68 | left, top, right, bottom = gc.GetStringBounds(label)
69 | gc.FillStringAt(label, 72/2-((right-left)/2), 72-((bottom-top)/2))
70 |
71 | deck.WriteImageToKey(dest, key)
72 | }
73 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component, createRef } from 'react';
2 | import classnames from 'classnames';
3 |
4 | class Login extends Component {
5 | static propTypes = {};
6 |
7 | state = {
8 | username: '',
9 | password: '',
10 | };
11 |
12 | constructor(props) {
13 | super(props);
14 | this.autofocusRef = createRef(null);
15 | }
16 |
17 | componentDidMount() {
18 | if (this.autofocusRef.current) {
19 | this.autofocusRef.current.focus();
20 | }
21 | }
22 |
23 | onChange = (field, value) => {
24 | this.setState({
25 | [field]: value,
26 | });
27 | };
28 |
29 | onSubmit = (e) => {
30 | e.preventDefault();
31 |
32 | this.props.onSubmit(this.state.username, this.state.password);
33 | };
34 |
35 | render() {
36 | const { username, password } = this.state;
37 | const { error } = this.props;
38 |
39 | return (
40 |
70 | );
71 | }
72 | }
73 |
74 | export default Login;
75 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/components/Register.js:
--------------------------------------------------------------------------------
1 | import React, { Component, createRef } from 'react';
2 | import classnames from 'classnames';
3 |
4 | class Register extends Component {
5 | static propTypes = {};
6 |
7 | state = {
8 | username: '',
9 | password: '',
10 | };
11 |
12 | constructor(props) {
13 | super(props);
14 | this.autofocusRef = createRef(null);
15 | }
16 |
17 | componentDidMount() {
18 | if (this.autofocusRef.current) {
19 | this.autofocusRef.current.focus();
20 | }
21 | }
22 |
23 | onChange = (field, value) => {
24 | this.setState({
25 | [field]: value,
26 | });
27 | };
28 |
29 | onSubmit = (e) => {
30 | e.preventDefault();
31 |
32 | this.props.onSubmit(this.state.username, this.state.password);
33 | };
34 |
35 | render() {
36 | const { username, password } = this.state;
37 | const { error } = this.props;
38 |
39 | return (
40 |
70 | );
71 | }
72 | }
73 |
74 | export default Register;
75 |
--------------------------------------------------------------------------------
/nodes/stampzilla-google-assistant/oauthhandler.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/RangelReale/osin"
8 | "github.com/gin-gonic/gin"
9 | "github.com/sirupsen/logrus"
10 | )
11 |
12 | func authorize(oauth2server *osin.Server) func(c *gin.Context) {
13 | return func(c *gin.Context) {
14 | resp := oauth2server.NewResponse()
15 | defer resp.Close()
16 |
17 | if ar := oauth2server.HandleAuthorizeRequest(resp, c.Request); ar != nil {
18 | // HANDLE LOGIN PAGE HERE
19 | if !handleLoginPage(ar, c.Writer, c.Request) {
20 | return
21 | }
22 | // ar.UserData = struct{ Login string }{Login: "test"}
23 | ar.Authorized = true
24 | oauth2server.FinishAuthorizeRequest(resp, c.Request, ar)
25 | }
26 | if resp.IsError && resp.InternalError != nil {
27 | logrus.Errorf("ERROR: %#v\n", resp.InternalError)
28 | }
29 | osin.OutputJSON(resp, c.Writer, c.Request)
30 | }
31 | }
32 |
33 | func token(oauth2server *osin.Server) func(c *gin.Context) {
34 | return func(c *gin.Context) {
35 | resp := oauth2server.NewResponse()
36 | defer resp.Close()
37 |
38 | if ar := oauth2server.HandleAccessRequest(resp, c.Request); ar != nil {
39 | ar.Authorized = true
40 | oauth2server.FinishAccessRequest(resp, c.Request, ar)
41 | }
42 | if resp.IsError && resp.InternalError != nil {
43 | logrus.Errorf("ERROR: %#v\n", resp.InternalError)
44 | }
45 | osin.OutputJSON(resp, c.Writer, c.Request)
46 | }
47 | }
48 |
49 | func handleLoginPage(ar *osin.AuthorizeRequest, w http.ResponseWriter, r *http.Request) bool {
50 | r.ParseForm()
51 | if r.Method == "POST" && r.Form.Get("login") == "test" && r.Form.Get("password") == "test" {
52 | return true
53 | }
54 |
55 | w.Write([]byte(""))
56 |
57 | w.Write([]byte(fmt.Sprintf("LOGIN %s (use test/test)
", ar.Client.GetId())))
58 | w.Write([]byte(fmt.Sprintf(""))
65 |
66 | w.Write([]byte(""))
67 |
68 | return false
69 | }
70 |
--------------------------------------------------------------------------------
/nodes/stampzilla-google-assistant/googleassistant/request.go:
--------------------------------------------------------------------------------
1 | package googleassistant
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/sirupsen/logrus"
7 | )
8 |
9 | // Intent of the action.
10 | type Intent string
11 |
12 | const (
13 | // SyncIntent is used when syncing devices.
14 | SyncIntent = "action.devices.SYNC"
15 | // QueryIntent is used when querying for status.
16 | QueryIntent = "action.devices.QUERY"
17 | // ExecuteIntent is used when controlling devices.
18 | ExecuteIntent = "action.devices.EXECUTE"
19 |
20 | CommandBrightnessAbsolute = "action.devices.commands.BrightnessAbsolute"
21 | CommandOnOff = "action.devices.commands.OnOff"
22 | )
23 |
24 | // Inputs from google.
25 | type Inputs []map[string]json.RawMessage
26 |
27 | func (i Inputs) Intent() Intent {
28 | for _, v := range i {
29 | if v, ok := v["intent"]; ok {
30 | in := ""
31 | err := json.Unmarshal(v, &in)
32 | if err != nil {
33 | logrus.Error(err)
34 | return ""
35 | }
36 |
37 | return Intent(in)
38 | }
39 | }
40 | return ""
41 | }
42 |
43 | func (i Inputs) Payload() Payload {
44 | for _, v := range i {
45 | if v, ok := v["payload"]; ok {
46 | pl := Payload{}
47 | err := json.Unmarshal(v, &pl)
48 | if err != nil {
49 | logrus.Error(err)
50 | return Payload{}
51 | }
52 | return pl
53 | }
54 | }
55 | return Payload{}
56 | }
57 |
58 | type Payload struct {
59 | Commands []struct {
60 | Devices []struct {
61 | ID string `json:"id"`
62 | // CustomData struct {
63 | // FooValue int `json:"fooValue"`
64 | // BarValue bool `json:"barValue"`
65 | // BazValue string `json:"bazValue"`
66 | // } `json:"customData"`
67 | } `json:"devices"`
68 | Execution []struct {
69 | Command string `json:"command"`
70 | Params struct {
71 | On bool `json:"on"`
72 | Brightness int `json:"brightness"`
73 | } `json:"params"`
74 | } `json:"execution"`
75 | } `json:"commands"`
76 | Devices []struct {
77 | ID string `json:"id"`
78 | // CustomData struct {
79 | // FooValue int `json:"fooValue"`
80 | // BarValue bool `json:"barValue"`
81 | // BazValue string `json:"bazValue"`
82 | // } `json:"customData"`
83 | } `json:"devices"`
84 | }
85 |
86 | type Request struct {
87 | RequestID string
88 | Inputs Inputs
89 | }
90 |
--------------------------------------------------------------------------------
/nodes/stampzilla-knx/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | "github.com/vapourismo/knx-go/knx"
8 | "github.com/vapourismo/knx-go/knx/cemi"
9 | "github.com/vapourismo/knx-go/knx/dpt"
10 | )
11 |
12 | type config struct {
13 | Gateway gateway `json:"gateway"`
14 | Lights lights `json:"lights"`
15 | Sensors []sensor `json:"sensors"`
16 | sync.Mutex
17 | }
18 |
19 | func (c *config) GetLight(id string) *light {
20 | c.Lock()
21 | defer c.Unlock()
22 | for _, v := range c.Lights {
23 | if v.ID == id {
24 | return &v
25 | }
26 | }
27 | return nil
28 | }
29 |
30 | type gateway struct {
31 | Address string `json:"address"`
32 | }
33 |
34 | type lights []light
35 |
36 | type light struct {
37 | ID string `json:"id"`
38 | ControlSwitch string `json:"control_switch"`
39 | ControlBrightness string `json:"control_brightness"`
40 | StateSwitch string `json:"state_switch"`
41 | StateBrightness string `json:"state_brightness"`
42 | }
43 |
44 | type sensor struct {
45 | ID string `json:"id"`
46 | Motion string `json:"motion"`
47 | MotionTrue string `json:"motionTrue"`
48 | Lux string `json:"lux"`
49 | Temperature string `json:"temperature"`
50 | Humidity string `json:"humidity"`
51 | Co2 string `json:"co2"`
52 | Voc string `json:"voc"`
53 | AirPressure string `json:"airPressure"`
54 | DewPoint string `json:"dewPoint"`
55 | }
56 |
57 | func (light *light) Switch(tunnel *tunnel, target bool) error {
58 | if !tunnel.Connected() {
59 | return fmt.Errorf("not connected to KNX gateway")
60 | }
61 | addr, err := cemi.NewGroupAddrString(light.ControlSwitch)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | cmd := knx.GroupEvent{
67 | Command: knx.GroupWrite,
68 | Destination: addr,
69 | Data: dpt.DPT_1001(target).Pack(),
70 | }
71 | return tunnel.Send(cmd)
72 | }
73 |
74 | func (light *light) Brightness(tunnel *tunnel, target float64) error {
75 | if !tunnel.Connected() {
76 | return fmt.Errorf("not connected to KNX gateway")
77 | }
78 | addr, err := cemi.NewGroupAddrString(light.ControlBrightness)
79 | if err != nil {
80 | return err
81 | }
82 |
83 | cmd := knx.GroupEvent{
84 | Command: knx.GroupWrite,
85 | Destination: addr,
86 | Data: dpt.DPT_5001(float32(target)).Pack(),
87 | }
88 | return tunnel.Send(cmd)
89 | }
90 |
--------------------------------------------------------------------------------
/nodes/stampzilla-nx-witness/api.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 |
10 | "github.com/sirupsen/logrus"
11 | )
12 |
13 | type API struct {
14 | config *Config
15 | }
16 |
17 | func NewAPI(config *Config) *API {
18 | return &API{
19 | config: config,
20 | }
21 | }
22 |
23 | func (api *API) fetchEventRules() (EventRulesResponse, error) {
24 | url := fmt.Sprintf("%s/ec2/getEventRules", api.config.Host)
25 | req, err := http.NewRequest("GET", url, nil)
26 | if err != nil {
27 | return nil, fmt.Errorf("fetchEventRules: error create request: %w", err)
28 | }
29 |
30 | req.Header.Set("Content-Type", "application/json")
31 | req.SetBasicAuth(api.config.Username, api.config.Password)
32 |
33 | resp, err := httpClient.Do(req)
34 | if err != nil {
35 | return nil, err
36 | }
37 | defer resp.Body.Close()
38 |
39 | if resp.StatusCode != 200 {
40 | return nil, fmt.Errorf("error fetching from api status: %d", resp.StatusCode)
41 | }
42 |
43 | response := EventRulesResponse{}
44 | err = json.NewDecoder(resp.Body).Decode(&response)
45 | if err != nil {
46 | return nil, err
47 | }
48 | return response, nil
49 | }
50 |
51 | func (api *API) saveEventRule(rule Rule) error {
52 | url := fmt.Sprintf("%s/ec2/saveEventRule", api.config.Host)
53 | data, err := json.Marshal(&rule)
54 | if err != nil {
55 | return fmt.Errorf("saveEventRule: error marshal json: %w", err)
56 | }
57 |
58 | req, err := http.NewRequest("POST", url, bytes.NewReader(data))
59 | if err != nil {
60 | return fmt.Errorf("saveEventRule: error creating request: %w", err)
61 | }
62 |
63 | req.Header.Set("Content-Type", "application/json")
64 | req.SetBasicAuth(api.config.Username, api.config.Password)
65 |
66 | logrus.Debugf("req to url: %s", url)
67 |
68 | resp, err := httpClient.Do(req)
69 | if err != nil {
70 | return fmt.Errorf("saveEventRule: error making request: %w", err)
71 | }
72 |
73 | defer resp.Body.Close()
74 | b, err := ioutil.ReadAll(resp.Body)
75 | if err != nil {
76 | return fmt.Errorf("saveEventRule: error reading body: %w", err)
77 | }
78 |
79 | logrus.Debugf("statusCode: %s", resp.Status)
80 | logrus.Debugf("body: %s", b)
81 |
82 | if resp.StatusCode != 200 {
83 | return fmt.Errorf("error saving to api, status: %d, body: %s", resp.StatusCode, string(b))
84 | }
85 |
86 | return nil
87 | }
88 |
--------------------------------------------------------------------------------
/nodes/stampzilla-google-assistant/googleassistant/response.go:
--------------------------------------------------------------------------------
1 | package googleassistant
2 |
3 | type DeviceName struct {
4 | DefaultNames []string `json:"defaultNames,omitempty"`
5 | Name string `json:"name,omitempty"`
6 | Nicknames []string `json:"nicknames,omitempty"`
7 | }
8 |
9 | type DeviceAttributes struct {
10 | ColorModel string `json:"colorModel,omitempty"`
11 | TemperatureMinK int `json:"temperatureMinK,omitempty"`
12 | TemperatureMaxK int `json:"temperatureMaxK,omitempty"`
13 | }
14 |
15 | type Device struct {
16 | ID string `json:"id"`
17 | Type string `json:"type"`
18 | Traits []string `json:"traits"`
19 | Name DeviceName `json:"name"`
20 | WillReportState bool `json:"willReportState"`
21 | // DeviceInfo struct {
22 | // Manufacturer string `json:"manufacturer"`
23 | // Model string `json:"model"`
24 | // HwVersion string `json:"hwVersion"`
25 | // SwVersion string `json:"swVersion"`
26 | // } `json:"deviceInfo"`
27 | // CustomData struct {
28 | // FooValue int `json:"fooValue"`
29 | // BarValue bool `json:"barValue"`
30 | // BazValue string `json:"bazValue"`
31 | // } `json:"customData"`
32 | Attributes DeviceAttributes `json:"attributes,omitempty"`
33 | }
34 |
35 | type ResponseStates struct {
36 | On bool `json:"on,omitempty"`
37 | Brightness int `json:"brightness,omitempty"`
38 | Online bool `json:"online,omitempty"`
39 | }
40 |
41 | func NewResponseCommand() ResponseCommand {
42 | return ResponseCommand{
43 | States: ResponseStates{},
44 | Status: "SUCCESS",
45 | }
46 | }
47 |
48 | type ResponseCommand struct {
49 | IDs []string `json:"ids"`
50 | Status string `json:"status"`
51 | States ResponseStates `json:"states"`
52 | ErrorCode string `json:"errorCode,omitempty"`
53 | }
54 |
55 | type Response struct {
56 | RequestID string `json:"requestId"`
57 | Payload struct {
58 | AgentUserID string `json:"agentUserId,omitempty"`
59 | Devices []Device `json:"devices,omitempty"`
60 | Commands []ResponseCommand `json:"commands,omitempty"`
61 | } `json:"payload"`
62 | }
63 |
64 | type QueryResponse struct {
65 | RequestID string `json:"requestId"`
66 | Payload struct {
67 | Devices map[string]map[string]interface{} `json:"devices,omitempty"`
68 | } `json:"payload"`
69 | }
70 |
--------------------------------------------------------------------------------
/nodes/stampzilla-mbus/worker.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net"
9 | "os"
10 | "sync"
11 | "syscall"
12 | "time"
13 |
14 | "github.com/jonaz/gombus"
15 | "github.com/sirupsen/logrus"
16 | )
17 |
18 | type Worker struct {
19 | wg sync.WaitGroup
20 | work chan func(*gombus.Conn) error
21 | reconnect chan struct{}
22 | cancel context.CancelFunc
23 | conn *gombus.Conn
24 | config *Config
25 | }
26 |
27 | func NewWorker(config *Config) *Worker {
28 | return &Worker{
29 | work: make(chan func(*gombus.Conn) error),
30 | reconnect: make(chan struct{}),
31 | config: config,
32 | }
33 | }
34 |
35 | func (w *Worker) Do(fn func(*gombus.Conn) error) {
36 | w.work <- fn
37 | }
38 |
39 | func (w *Worker) Start(parentCtx context.Context, workers int) {
40 | var ctx context.Context
41 | ctx, w.cancel = context.WithCancel(parentCtx)
42 |
43 | go func() {
44 | defer w.wg.Done()
45 | for {
46 | select {
47 | case <-ctx.Done():
48 | return
49 | case <-w.reconnect:
50 | for {
51 | err := w.connectTCP()
52 | if err != nil {
53 | logrus.Error(err)
54 | time.Sleep(time.Second * 1)
55 | continue
56 | }
57 | break
58 | }
59 | }
60 | }
61 | }()
62 | w.reconnect <- struct{}{}
63 | for i := 0; i < workers; i++ {
64 | w.wg.Add(1)
65 | go w.start(ctx)
66 | }
67 | }
68 |
69 | func (w *Worker) connectTCP() error {
70 | var err error
71 | w.conn, err = gombus.Dial(net.JoinHostPort(w.config.Host, w.config.Port))
72 | if err != nil {
73 | return fmt.Errorf("error connecting to mbus: %w", err)
74 | }
75 |
76 | if conn, ok := w.conn.Conn().(*net.TCPConn); ok {
77 | er := conn.SetKeepAlive(true)
78 | if er != nil {
79 | return er
80 | }
81 |
82 | er = conn.SetKeepAlivePeriod(time.Second * 10)
83 | if er != nil {
84 | return er
85 | }
86 | }
87 |
88 | return nil
89 | }
90 |
91 | func (w *Worker) start(ctx context.Context) {
92 | defer w.wg.Done()
93 |
94 | for {
95 | select {
96 | case <-ctx.Done():
97 | return
98 | case fn := <-w.work:
99 | err := fn(w.conn)
100 | if err != nil {
101 | logrus.Error(err)
102 | }
103 |
104 | if os.IsTimeout(err) || errors.Is(err, io.EOF) || errors.Is(err, syscall.EPIPE) {
105 | w.reconnect <- struct{}{}
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/nodes/stampzilla-husdata-h60/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 | "time"
9 |
10 | "github.com/gorilla/websocket"
11 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/e2e"
12 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestUpdateState(t *testing.T) {
17 | main, _, cleanup := e2e.SetupWebsocketTest(t)
18 | defer cleanup()
19 | e2e.AcceptCertificateRequest(t, main)
20 |
21 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 | // Assert we called our heatpump with the correct parameters
23 | if r.URL.Query().Get("idx") == "0203" {
24 | assert.Equal(t, "/api/set?idx=0203&val=225", r.URL.String())
25 | return
26 | }
27 | if r.URL.Query().Get("idx") == "6209" {
28 | assert.Equal(t, "/api/set?idx=6209&val=2", r.URL.String())
29 | return
30 | }
31 | t.Error("unexpected get parameters")
32 | }))
33 | defer ts.Close()
34 |
35 | config := NewConfig()
36 | config.Host = ts.URL
37 | node := setupNode(config)
38 |
39 | err := node.Connect()
40 | assert.NoError(t, err)
41 |
42 | dev := &devices.Device{
43 | Name: "heatpump",
44 | Type: "sensor",
45 | ID: devices.ID{ID: "1"},
46 | Online: true,
47 | Traits: []string{"TemperatureControl"},
48 | State: make(devices.State),
49 | }
50 | node.AddOrUpdate(dev)
51 |
52 | b := []byte(fmt.Sprintf(`
53 | {
54 | "type": "state-change",
55 | "body": {
56 | "%s.1": {
57 | "type": "light",
58 | "id": "%s.1",
59 | "name": "heatpump",
60 | "online": true,
61 | "state": {
62 | "RoomTempSetpoint": 22.5,
63 | "ExtraWarmWater": 2
64 | }
65 | }
66 | }
67 | }
68 | `, node.UUID, node.UUID))
69 |
70 | err = node.Client.WriteMessage(websocket.TextMessage, b)
71 | assert.NoError(t, err)
72 | var syncedDev *devices.Device
73 | e2e.WaitFor(t, 1*time.Second, "wait for node to have updated RoomTempSetpoint", func() bool {
74 | syncedDev = node.GetDevice("1")
75 | if dev != nil && syncedDev.State["RoomTempSetpoint"] != nil {
76 | return true
77 | }
78 | return false
79 | })
80 | assert.Equal(t, 22.5, syncedDev.State["RoomTempSetpoint"])
81 | assert.Equal(t, float64(2), syncedDev.State["ExtraWarmWater"])
82 | }
83 |
--------------------------------------------------------------------------------
/nodes/stampzilla-linux/dpms.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "os"
8 | "os/exec"
9 | "regexp"
10 | "strings"
11 | "time"
12 |
13 | "github.com/sirupsen/logrus"
14 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
15 | )
16 |
17 | func startMonitorDpms() {
18 | c1 := exec.Command("ls", "/tmp/.X11-unix")
19 | c2 := exec.Command("tr", "X", ":")
20 | r, w := io.Pipe()
21 | c1.Stdout = w
22 | c2.Stdin = r
23 |
24 | var b2 bytes.Buffer
25 | c2.Stdout = &b2
26 |
27 | c1.Start()
28 | c2.Start()
29 | c1.Wait()
30 | w.Close()
31 | c2.Wait()
32 | result := strings.Split(strings.TrimSpace(b2.String()), "\n")
33 |
34 | for _, screen := range result {
35 | go monitorDpms(screen)
36 | }
37 | }
38 |
39 | func monitorDpms(screen string) {
40 | dev := &devices.Device{
41 | Name: "Monitor " + screen,
42 | ID: devices.ID{ID: "monitor" + screen},
43 | Online: true,
44 | Traits: []string{"OnOff"},
45 | State: devices.State{
46 | "on": false,
47 | },
48 | }
49 | added := false
50 |
51 | re := regexp.MustCompile("Monitor is (in )?([^ \n]+)")
52 |
53 | for {
54 | cmd := exec.Command("xset", "q")
55 | cmd.Env = append(os.Environ(), "DISPLAY="+screen)
56 | var stderr bytes.Buffer
57 | cmd.Stderr = &stderr
58 | out, err := cmd.Output()
59 | if err != nil {
60 | logrus.Errorf("Failed to read monitor status: %s: %s", fmt.Sprint(err), stderr.String())
61 | return
62 | }
63 |
64 | if !added {
65 | n.AddOrUpdate(dev)
66 | added = true
67 | }
68 |
69 | status := re.FindStringSubmatch(string(out))
70 | if len(status) > 2 {
71 | newState := make(devices.State)
72 | newState["monitor_status"] = status[2]
73 | newState["on"] = status[2] == "On"
74 | n.UpdateState(dev.ID.ID, newState)
75 | }
76 | <-time.After(time.Second * 1)
77 | }
78 | }
79 |
80 | func changeDpmsState(screen string, state bool) error {
81 | if state {
82 | cmd := exec.Command("xset", "dpms", "force", "on")
83 | cmd.Env = append(os.Environ(), "DISPLAY="+screen)
84 | _, err := cmd.Output()
85 | if err != nil {
86 | return err
87 | }
88 | }
89 | if !state {
90 | cmd := exec.Command("xset", "dpms", "force", "off")
91 | cmd.Env = append(os.Environ(), "DISPLAY="+screen)
92 | _, err := cmd.Output()
93 | if err != nil {
94 | return err
95 | }
96 | }
97 |
98 | return nil
99 | }
100 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/destination_test.go:
--------------------------------------------------------------------------------
1 | package notification
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | "os"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestEqual(t *testing.T) {
13 | d1 := &Destination{
14 | UUID: "uuid1",
15 | Type: "type1",
16 | Name: "name1",
17 | Sender: "sender1",
18 | Destinations: []string{
19 | "a", "b",
20 | },
21 | }
22 | d2 := &Destination{
23 | UUID: "uuid1",
24 | Type: "type1",
25 | Name: "name1",
26 | Sender: "sender1",
27 | Destinations: []string{
28 | "a", "b",
29 | },
30 | }
31 | assert.True(t, d1.Equal(d2))
32 |
33 | d3 := &Destination{
34 | UUID: "uuid1",
35 | Type: "type1",
36 | Name: "name1",
37 | Sender: "sender2",
38 | }
39 | assert.False(t, d1.Equal(d3))
40 |
41 | d3.Sender = d1.Sender
42 | d3.Name = "name3"
43 |
44 | assert.False(t, d1.Equal(d3))
45 |
46 | d3.Name = d1.Name
47 | d3.Type = "type3"
48 |
49 | assert.False(t, d1.Equal(d3))
50 |
51 | d3.Type = d1.Type
52 | d3.UUID = "uuid3"
53 |
54 | assert.False(t, d1.Equal(d3))
55 |
56 | d3.UUID = d1.UUID
57 | d3.Destinations = []string{
58 | "a",
59 | }
60 |
61 | assert.False(t, d1.Equal(d3))
62 |
63 | d3.Destinations = []string{
64 | "a", "c",
65 | }
66 |
67 | assert.False(t, d1.Equal(d3))
68 | }
69 |
70 | func TestAddDestination(t *testing.T) {
71 | dests := NewDestinations()
72 | d1 := &Destination{
73 | Name: "name",
74 | UUID: "uuid",
75 | }
76 | dests.Add(d1)
77 | assert.Equal(t, "name", dests.Get("uuid").Name)
78 | }
79 |
80 | func TestRemoveDestination(t *testing.T) {
81 | dests := NewDestinations()
82 | d1 := &Destination{
83 | Name: "name",
84 | UUID: "uuid",
85 | }
86 | dests.Add(d1)
87 | dests.Remove("uuid")
88 | assert.Len(t, dests.All(), 0)
89 | }
90 |
91 | func TestReadWrite(t *testing.T) {
92 | file, err := ioutil.TempFile("", "readwritedestinations")
93 | if err != nil {
94 | log.Fatal(err)
95 | }
96 | defer os.Remove(file.Name())
97 |
98 | ds1 := NewDestinations()
99 | ds2 := NewDestinations()
100 |
101 | d1 := &Destination{
102 | Name: "name",
103 | UUID: "uuid",
104 | }
105 | ds1.Add(d1)
106 |
107 | err = ds1.Save(file.Name())
108 | assert.NoError(t, err)
109 |
110 | err = ds2.Load(file.Name())
111 | assert.NoError(t, err)
112 |
113 | d2 := ds2.Get("uuid")
114 |
115 | assert.True(t, d1.Equal(d2))
116 | }
117 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/persons/person.go:
--------------------------------------------------------------------------------
1 | package persons
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
8 | "golang.org/x/crypto/bcrypt"
9 | )
10 |
11 | type Person struct {
12 | UUID string `json:"uuid"`
13 | Name string `json:"name"`
14 | Username string `json:"username"`
15 | Email string `json:"email"`
16 | AllowLogin bool `json:"allow_login"`
17 | IsAdmin bool `json:"is_admin"`
18 |
19 | LastSeen time.Time `json:"last_seen"`
20 |
21 | State devices.State `json:"state"`
22 | }
23 |
24 | type PersonWithPassword struct {
25 | Person
26 | Password string `json:"password"`
27 | }
28 |
29 | type PersonWithPasswords struct {
30 | PersonWithPassword
31 | NewPassword string `json:"new_password,omitempty"`
32 | RepeatPassword string `json:"repeat_password,omitempty"`
33 | }
34 |
35 | // Equal checks if 2 persons are equal.
36 | func (a Person) Equal(b PersonWithPasswords) bool {
37 | if !a.State.Equal(b.State) { // this is most likely to not be equal so we check it first
38 | return false
39 | }
40 | if a.Name != b.Name {
41 | return false
42 | }
43 | if a.Username != b.Username {
44 | return false
45 | }
46 | if a.Email != b.Email {
47 | return false
48 | }
49 | if b.Password != "" {
50 | return false
51 | }
52 | if a.LastSeen != b.LastSeen {
53 | return false
54 | }
55 | if a.AllowLogin != b.AllowLogin {
56 | return false
57 | }
58 | if a.IsAdmin != b.IsAdmin {
59 | return false
60 | }
61 | if b.NewPassword != "" {
62 | return false
63 | }
64 |
65 | return true
66 | }
67 |
68 | func (a *PersonWithPasswords) UpdatePassword() error {
69 | if a.NewPassword != "" && a.NewPassword != a.RepeatPassword {
70 | return fmt.Errorf("repeat password does not match the new password")
71 | }
72 |
73 | // Change password
74 | if a.NewPassword != "" && a.NewPassword == a.RepeatPassword {
75 | hash, err := bcrypt.GenerateFromPassword([]byte(a.NewPassword), bcrypt.DefaultCost)
76 | if err != nil {
77 | return fmt.Errorf("failed to generate password hash: %s", err)
78 | }
79 |
80 | a.Password = string(hash)
81 | }
82 |
83 | a.NewPassword = ""
84 | a.RepeatPassword = ""
85 |
86 | return nil
87 | }
88 |
89 | func (p *PersonWithPassword) CheckPassword(password string) error {
90 | if err := bcrypt.CompareHashAndPassword([]byte(p.Password), []byte(password)); err != nil {
91 | return fmt.Errorf("wrong username or password")
92 | }
93 |
94 | return nil
95 | }
96 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/devices/state.go:
--------------------------------------------------------------------------------
1 | package devices
2 |
3 | type State map[string]interface{}
4 |
5 | func (ds State) Clone() State {
6 | newState := make(State)
7 | for k, v := range ds {
8 | newState[k] = v
9 | }
10 | return newState
11 | }
12 |
13 | // Bool runs fn only if key is found in map and it is of type bool.
14 | func (ds State) Bool(key string, fn func(bool)) {
15 | if v, ok := ds[key]; ok {
16 | if v, ok := v.(bool); ok {
17 | fn(v)
18 | }
19 | }
20 | }
21 |
22 | // Int runs fn only if key is found in map and it is of type int.
23 | func (ds State) Int(key string, fn func(int64)) {
24 | if v, ok := ds[key]; ok {
25 | if v, ok := v.(int); ok {
26 | fn(int64(v))
27 | }
28 | if v, ok := v.(int64); ok {
29 | fn(v)
30 | }
31 | }
32 | }
33 |
34 | // Float runs fn only if key is found in map and it is of type float64.
35 | func (ds State) Float(key string, fn func(float64)) {
36 | if v, ok := ds[key]; ok {
37 | if v, ok := v.(float64); ok {
38 | fn(v)
39 | }
40 | }
41 | }
42 |
43 | // String runs fn only if key is found in map and it is of type string.
44 | func (ds State) String(key string, fn func(string)) {
45 | if v, ok := ds[key]; ok {
46 | if v, ok := v.(string); ok {
47 | fn(v)
48 | }
49 | }
50 | }
51 |
52 | // Diff calculates the diff between 2 states. If key is missing in right but exists in left it will not be a diff.
53 | func (ds State) Diff(right State) State {
54 | diff := make(State)
55 | for k, v := range ds {
56 | rv, ok := right[k]
57 | if !ok {
58 | // diff[k] = v
59 | continue
60 | }
61 | if ok && v != rv {
62 | diff[k] = rv
63 | }
64 | }
65 |
66 | for k, v := range right {
67 | if _, ok := ds[k]; !ok {
68 | diff[k] = v
69 | }
70 | }
71 |
72 | return diff
73 | }
74 |
75 | // Merge two states.
76 | func (ds State) Merge(right State) State {
77 | diff := make(State)
78 | for k, v := range ds {
79 | diff[k] = v
80 | }
81 | for k, v := range right {
82 | diff[k] = v
83 | }
84 | return diff
85 | }
86 |
87 | func (ds State) MergeWith(right State) {
88 | for k, v := range right {
89 | ds[k] = v
90 | }
91 | }
92 |
93 | func (ds State) Equal(right State) bool {
94 | if (ds == nil) != (right == nil) {
95 | return false
96 | }
97 |
98 | if len(ds) != len(right) {
99 | return false
100 | }
101 | for k := range ds {
102 | if ds[k] != right[k] {
103 | return false
104 | }
105 | }
106 | return true
107 | }
108 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/models/notification/file/file_test.go:
--------------------------------------------------------------------------------
1 | package file
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "log"
7 | "os"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestTrigger(t *testing.T) {
14 | f := New(json.RawMessage("{\"append\": false, \"timestamp\": false}"))
15 |
16 | file, err := ioutil.TempFile("", "testTrigger")
17 | if err != nil {
18 | log.Fatal(err)
19 | }
20 | defer os.Remove(file.Name())
21 |
22 | err = f.Trigger([]string{file.Name()}, "test1")
23 | assert.NoError(t, err)
24 |
25 | err = f.Trigger([]string{file.Name()}, "test2")
26 | assert.NoError(t, err)
27 |
28 | h, err := os.Open(file.Name())
29 | assert.NoError(t, err)
30 | defer h.Close()
31 |
32 | b, err := ioutil.ReadAll(h)
33 |
34 | assert.Equal(t, b, []byte("test2\tTriggered\r\n"))
35 | }
36 |
37 | func TestTriggerAppend(t *testing.T) {
38 | f := New(json.RawMessage("{\"append\": true, \"timestamp\": false}"))
39 |
40 | file, err := ioutil.TempFile("", "testTrigger")
41 | if err != nil {
42 | log.Fatal(err)
43 | }
44 | defer os.Remove(file.Name())
45 |
46 | err = f.Trigger([]string{file.Name()}, "test1")
47 | assert.NoError(t, err)
48 |
49 | err = f.Release([]string{file.Name()}, "test2")
50 | assert.NoError(t, err)
51 |
52 | h, err := os.Open(file.Name())
53 | assert.NoError(t, err)
54 | defer h.Close()
55 |
56 | b, err := ioutil.ReadAll(h)
57 |
58 | assert.Equal(t, b, []byte("test1\tTriggered\r\ntest2\tReleased\r\n"))
59 | }
60 |
61 | func TestTriggerTimestamp(t *testing.T) {
62 | f := New(json.RawMessage("{\"append\": false, \"timestamp\": true}"))
63 |
64 | file, err := ioutil.TempFile("", "testTrigger")
65 | if err != nil {
66 | log.Fatal(err)
67 | }
68 | defer os.Remove(file.Name())
69 |
70 | err = f.Trigger([]string{file.Name()}, "test1")
71 | assert.NoError(t, err)
72 |
73 | h, err := os.Open(file.Name())
74 | assert.NoError(t, err)
75 | defer h.Close()
76 |
77 | b, err := ioutil.ReadAll(h)
78 |
79 | assert.Equal(t, len(b), 37)
80 | }
81 |
82 | func TestNoFile(t *testing.T) {
83 | f := New(json.RawMessage(""))
84 |
85 | err := f.Trigger([]string{"/"}, "test1")
86 | assert.Error(t, err)
87 |
88 | err = f.Release([]string{"/"}, "test1")
89 | assert.Error(t, err)
90 | }
91 |
92 | func TestDestinations(t *testing.T) {
93 | f := New(json.RawMessage(""))
94 |
95 | d, err := f.Destinations()
96 |
97 | assert.Error(t, err)
98 | assert.Nil(t, d)
99 | }
100 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/logic/savedstate.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "sync"
8 |
9 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
10 | )
11 |
12 | /* savedstate.json example
13 | {
14 | "6fbaea24-6b3f-4856-9194-735b349bbf4d": {
15 | "name": "test state 1",
16 | "uuid": "6fbaea24-6b3f-4856-9194-735b349bbf4d",
17 | "state": {
18 | "nodeuuid.deviceid": {
19 | "on": true
20 | }
21 | }
22 | }
23 | }
24 | */
25 |
26 | type SavedStates map[string]*SavedState
27 |
28 | type SavedState struct {
29 | Name string `json:"name"`
30 | UUID string `json:"uuid"`
31 | State map[devices.ID]devices.State `json:"state"`
32 | }
33 |
34 | type SavedStateStore struct {
35 | State SavedStates
36 | sync.RWMutex
37 | }
38 |
39 | func (sss *SavedStateStore) Get(id string) *SavedState {
40 | sss.RLock()
41 | defer sss.RUnlock()
42 | return sss.State[id]
43 | }
44 |
45 | func (sss *SavedStateStore) All() SavedStates {
46 | sss.RLock()
47 | defer sss.RUnlock()
48 | return sss.State
49 | }
50 |
51 | func NewSavedStateStore() *SavedStateStore {
52 | return &SavedStateStore{
53 | State: make(map[string]*SavedState),
54 | }
55 | }
56 |
57 | func (sss *SavedStateStore) SetState(s SavedStates) {
58 | sss.Lock()
59 | sss.State = s
60 | sss.Unlock()
61 | }
62 |
63 | func (sss *SavedStateStore) Save() error {
64 | sss.Lock()
65 | defer sss.Unlock()
66 | configFile, err := os.Create("savedstate.json")
67 | if err != nil {
68 | return fmt.Errorf("savedstate: error saving savedstate.json: %s", err.Error())
69 | }
70 | encoder := json.NewEncoder(configFile)
71 | encoder.SetIndent("", "\t")
72 | err = encoder.Encode(sss.State)
73 | if err != nil {
74 | return fmt.Errorf("savedstate: error saving savedstate.json: %s", err.Error())
75 | }
76 | return nil
77 | }
78 |
79 | func (sss *SavedStateStore) Load() error {
80 | configFile, err := os.Open("savedstate.json")
81 | if err != nil {
82 | if os.IsNotExist(err) {
83 | return nil // We dont want to error our if the file does not exist when we start the server
84 | }
85 | return fmt.Errorf("savedstate: error loading savedstate.json: %s", err.Error())
86 | }
87 |
88 | sss.Lock()
89 | defer sss.Unlock()
90 | jsonParser := json.NewDecoder(configFile)
91 | if err = jsonParser.Decode(&sss.State); err != nil {
92 | return fmt.Errorf("savedstate: error loading savedstate.json: %s", err.Error())
93 | }
94 | return nil
95 | }
96 |
--------------------------------------------------------------------------------
/nodes/stampzilla-knx/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "strings"
8 |
9 | "github.com/sirupsen/logrus"
10 | "github.com/stampzilla/stampzilla-go/v2/nodes/stampzilla-server/models/devices"
11 | "github.com/stampzilla/stampzilla-go/v2/pkg/node"
12 | )
13 |
14 | func main() {
15 | node := node.New("knx")
16 |
17 | tunnel := newTunnel(node)
18 | tunnel.OnConnect = func() {
19 | for _, dev := range node.Devices.All() {
20 | dev.SetOnline(true)
21 | }
22 | node.SyncDevices()
23 | }
24 | tunnel.OnDisconnect = func() {
25 | for _, dev := range node.Devices.All() {
26 | dev.SetOnline(false)
27 | }
28 | node.SyncDevices()
29 | }
30 |
31 | config := &config{}
32 |
33 | node.OnConfig(updatedConfig(node, tunnel, config))
34 | node.OnRequestStateChange(func(state devices.State, device *devices.Device) error {
35 | id := strings.SplitN(device.ID.ID, ".", 2)
36 |
37 | switch id[0] {
38 | case "light":
39 |
40 | light := config.GetLight(id[1])
41 | state.Bool("on", func(on bool) {
42 | err := light.Switch(tunnel, on)
43 | if err != nil {
44 | logrus.Error()
45 | }
46 | })
47 | state.Float("brightness", func(v float64) {
48 | err := light.Brightness(tunnel, v*100)
49 | if err != nil {
50 | logrus.Error()
51 | }
52 | })
53 |
54 | default:
55 | return fmt.Errorf("Unknown device type \"%s\"", id[0])
56 | }
57 | return nil
58 | })
59 |
60 | ctx, shutdown := context.WithCancel(context.Background())
61 | node.OnShutdown(func() {
62 | shutdown()
63 | })
64 |
65 | err := node.Connect()
66 | if err != nil {
67 | logrus.Error(err)
68 | return
69 | }
70 |
71 | tunnel.Start(ctx)
72 |
73 | logrus.SetFormatter(&logrus.TextFormatter{
74 | ForceColors: true,
75 | })
76 | logrus.SetReportCaller(false)
77 |
78 | tunnel.Wait()
79 | node.Wait()
80 | }
81 |
82 | func updatedConfig(node *node.Node, tunnel *tunnel, config *config) func(data json.RawMessage) error {
83 | return func(data json.RawMessage) error {
84 | config.Lock()
85 | defer config.Unlock()
86 | err := json.Unmarshal(data, config)
87 | if err != nil {
88 | return err
89 | }
90 |
91 | tunnel.SetAddress(config.Gateway.Address)
92 |
93 | tunnel.ClearAllLinks()
94 | for _, light := range config.Lights {
95 | setupLight(node, tunnel, light)
96 | }
97 |
98 | for _, sensor := range config.Sensors {
99 | setupSensor(node, tunnel, sensor)
100 | }
101 |
102 | return nil
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/pkg/installer/user.go:
--------------------------------------------------------------------------------
1 | package installer
2 |
3 | import (
4 | "os"
5 | "os/user"
6 | "strconv"
7 |
8 | "github.com/sirupsen/logrus"
9 | )
10 |
11 | func CreateUser(username string) {
12 | action := "Check user '" + username + "'"
13 | if userExists(username) {
14 | logrus.WithFields(logrus.Fields{
15 | "exists": "true",
16 | }).Debug(action)
17 | return
18 | }
19 |
20 | out, err := Run("useradd", "-m", "-r", "-s", "/bin/false", username)
21 | if err != nil {
22 | logrus.WithFields(logrus.Fields{
23 | "exists": "false",
24 | "error": err,
25 | "output": out,
26 | }).Panic(action)
27 | return
28 | }
29 |
30 | logrus.WithFields(logrus.Fields{
31 | "exists": "false",
32 | "created": "true",
33 | }).Info(action)
34 | }
35 |
36 | func userExists(username string) bool {
37 | _, err := Run("id", "-u", username)
38 | if err != nil {
39 | return false
40 | }
41 | return true
42 | }
43 |
44 | func CreateDirAsUser(directory string, username string) {
45 | action := "Check directory " + directory
46 | created := false
47 |
48 | if _, err := os.Stat(directory); os.IsNotExist(err) {
49 | err := os.MkdirAll(directory, 0777)
50 | if err != nil {
51 | logrus.WithFields(logrus.Fields{
52 | "exists": "false",
53 | "error": err,
54 | }).Panic(action)
55 | return
56 | }
57 | created = true
58 | }
59 |
60 | u, err := user.Lookup(username)
61 | if err != nil {
62 | logrus.WithFields(logrus.Fields{
63 | "exists": "true",
64 | "step": "User lookup",
65 | "error": err,
66 | }).Panic(action)
67 | return
68 | }
69 |
70 | uid, err := strconv.Atoi(u.Uid)
71 | if err != nil {
72 | logrus.WithFields(logrus.Fields{
73 | "exists": "true",
74 | "step": "strconv.Atoi(u.Uid)",
75 | "error": err,
76 | }).Panic(action)
77 | return
78 | }
79 | gid, err := strconv.Atoi(u.Gid)
80 | if err != nil {
81 | logrus.WithFields(logrus.Fields{
82 | "exists": "true",
83 | "step": "strconv.Atoi(u.Gid)",
84 | "error": err,
85 | }).Panic(action)
86 | return
87 | }
88 | err = os.Chown(directory, uid, gid)
89 | if err != nil {
90 | logrus.WithFields(logrus.Fields{
91 | "exists": "true",
92 | "step": "Set permissions",
93 | "error": err,
94 | }).Panic(action)
95 | return
96 | }
97 |
98 | if created {
99 | logrus.WithFields(logrus.Fields{
100 | "created": "true",
101 | }).Info(action)
102 | } else {
103 | logrus.WithFields(logrus.Fields{
104 | "exists": "true",
105 | }).Debug(action)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/nodes/stampzilla-server/web/src/routes/automation/components/StateEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { AlphaPicker } from 'react-color';
3 | import SliderPointer from 'react-color/lib/components/slider/SliderPointer';
4 | import Switch from 'react-switch';
5 | import ct from 'color-temperature';
6 |
7 | import { uniqeId } from '../../../helpers';
8 |
9 | // import HueColorPicker from './HueColorPicker';
10 |
11 | const temperatureToRgb = (temp) => {
12 | const color = ct.colorTemperature2rgb(temp);
13 | return `rgb(${color.red}, ${color.green}, ${color.blue})`;
14 | };
15 |
16 | const temperatureGradient = (start = 2000, end = 6000, steps = 10) => {
17 | const grad = [];
18 | for (let i = 0; i <= steps; i += 1) {
19 | const temp = ((end - start) / steps) * i;
20 | grad.push(`${temperatureToRgb(temp + start)} ${(100 / steps) * i}%`);
21 | }
22 |
23 | return grad.join(', ');
24 | };
25 |
26 | class StateEditor extends Component {
27 | renderStateEditor() {
28 | const {
29 | state, onChange, device, arrayKey,
30 | } = this.props;
31 | const id = uniqeId();
32 | const type = typeof state;
33 |
34 | switch (type) {
35 | case 'boolean':
36 | return (
37 |
44 | );
45 |
46 | case 'Brightness':
47 | case 'Volume':
48 | return (
49 | onChange(rgb.a)}
65 | disabled={!device.get('online') || !onChange}
66 | />
67 | );
68 | default:
69 | return (
70 | onChange(device, arrayKey, event.target.value)}
75 | />
76 | );
77 | }
78 | }
79 |
80 | render() {
81 | return <>{this.renderStateEditor()}>;
82 | }
83 | }
84 |
85 | export default StateEditor;
86 |
--------------------------------------------------------------------------------