├── frontend ├── src │ ├── styles │ │ ├── main-styles.scss │ │ └── fresh │ │ │ ├── core.scss │ │ │ └── partials │ │ │ ├── _hero.scss │ │ │ ├── _colors.scss │ │ │ ├── _footer.scss │ │ │ ├── _forms.scss │ │ │ ├── _responsive.scss │ │ │ ├── _blogteaser.scss │ │ │ ├── _testimonials.scss │ │ │ ├── _dropdowns.scss │ │ │ ├── _animations.scss │ │ │ ├── _cards.scss │ │ │ ├── _sections.scss │ │ │ ├── _buttons.scss │ │ │ ├── _navbar.scss │ │ │ └── _utils.scss │ ├── store │ │ └── index.js │ ├── app.js │ ├── consts.js │ ├── router.js │ ├── views │ │ ├── App.vue │ │ └── Main.vue │ └── assets │ │ └── logo_white.svg └── index.html ├── Makefile ├── .gitignore ├── vite-dev2.config.js ├── vite.config.js ├── package.json ├── .goreleaser.yml ├── go.mod ├── LICENSE ├── app ├── types.go ├── connection.go └── hems.go ├── README.md ├── main.go └── go.sum /frontend/src/styles/main-styles.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | //styles will go here 3 | @import "fresh/core"; 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default:: ui build 2 | 3 | clean:: 4 | rm -rf dist/ 5 | 6 | ui:: 7 | npm run build 8 | 9 | build:: 10 | go build 11 | 12 | snapshot:: 13 | goreleaser --snapshot --clean 14 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | 3 | export default createStore({ 4 | state: {}, 5 | getters: {}, 6 | mutations: {}, 7 | actions: {}, 8 | modules: {}, 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/app.js: -------------------------------------------------------------------------------- 1 | import "bulma/css/bulma.css"; 2 | import { createApp } from "vue"; 3 | import App from "./views/App.vue"; 4 | import setupRouter from "./router"; 5 | 6 | const app = createApp(App) 7 | app.use(setupRouter()) 8 | window.app = app.mount("#app") 9 | -------------------------------------------------------------------------------- /frontend/src/consts.js: -------------------------------------------------------------------------------- 1 | export const ConnectionStateEnum = { 2 | NONE: 0, 3 | QUEUED: 1, 4 | INITIATED: 2, 5 | RECEIVED: 3, 6 | INPROGRESS: 4, 7 | TRUSTED: 5, 8 | PIN: 6, 9 | COMPLETED: 7, 10 | REMOTEDENIEDTRUST: 8, 11 | ERROR: 9, 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Devices App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import MainView from "./views/Main.vue"; 3 | 4 | export default function setupRouter() { 5 | const router = createRouter({ 6 | history: createWebHashHistory(), 7 | routes: [ 8 | { 9 | path: "/", 10 | component: MainView, 11 | props: true, 12 | }, 13 | ] 14 | }); 15 | return router; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/views/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | -------------------------------------------------------------------------------- /vite-dev2.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | root: "./frontend", 7 | base: "./", 8 | server: { 9 | port: 7061, 10 | proxy: { 11 | "/ws": { target: "ws://localhost:7060", ws: true }, 12 | }, 13 | }, 14 | plugins: [vue()], 15 | css: { 16 | preprocessorOptions: { 17 | scss: { 18 | additionalData: `@import "./frontend/src/styles/main-styles.scss";` 19 | } 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | root: "./frontend", 7 | publicDir: "public", 8 | base: "./", 9 | build: { 10 | outDir: "../dist/", 11 | emptyOutDir: true, 12 | }, 13 | server: { 14 | port: 7051, 15 | proxy: { 16 | "/ws": { target: "ws://localhost:7050", ws: true }, 17 | }, 18 | }, 19 | plugins: [vue()], 20 | css: { 21 | preprocessorOptions: { 22 | scss: { 23 | additionalData: `@import "./frontend/src/styles/main-styles.scss";` 24 | } 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/core.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Main SCSS file / Fresh 3 | ========================================================================== */ 4 | 5 | //Imports 6 | @import 'partials/colors'; 7 | @import 'partials/navbar'; 8 | @import 'partials/dropdowns'; 9 | @import 'partials/sections'; 10 | @import 'partials/hero'; 11 | @import 'partials/footer'; 12 | @import 'partials/buttons'; 13 | @import 'partials/cards'; 14 | @import 'partials/forms'; 15 | @import 'partials/animations'; 16 | @import 'partials/blogteaser'; 17 | @import 'partials/testimonials'; 18 | @import 'partials/responsive'; 19 | @import 'partials/utils'; 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_hero.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Hero styles 3 | ========================================================================== */ 4 | 5 | .hero-body { 6 | padding-top: 6rem; 7 | padding-bottom: 6rem; 8 | .title, .subtitle { 9 | font-family: 'Open Sans', sans-serif; 10 | } 11 | .title { 12 | &.is-bold { 13 | font-weight: 700; 14 | } 15 | } 16 | .subtitle { 17 | &.is-muted { 18 | color: $muted-grey; 19 | } 20 | } 21 | } 22 | 23 | .hero-foot { 24 | img.partner-logo { 25 | height: 70px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_colors.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Color variables 3 | ========================================================================== */ 4 | 5 | $white: #fff; 6 | $smoke-white: #fcfcfc; 7 | $grey-white: #f2f2f2; 8 | 9 | $primary: #4FC1EA; 10 | $secondary: #56af33; 11 | // $secondary: #F39200; 12 | $accent: #56af33; 13 | // $accent: #00efb7; 14 | 15 | $fade-grey: #ededed; 16 | $light-grey: #EFF4F7; 17 | $title-grey: #9d9fa0; 18 | $blue-grey: #2d4a74; 19 | //$blue-grey: #444F60; 20 | $muted-grey: #999; 21 | $light-blue-grey: #98a9c3; 22 | $medium-grey: #66676b; 23 | $basaltic-grey: #878787; 24 | $purple: #7F00FF; 25 | $mint: #11FFBD; 26 | $bloody: #FC354C; 27 | $pinky: #ff00cc; 28 | $frost: #004e92; 29 | $placeholder: #cecece; 30 | $dark-grey: #344258; 31 | $border-grey: #ccc; 32 | $muted-grey: #999; 33 | $section-grey: #fbfbfb; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devices-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@histoire/plugin-vue": "^0.15.8", 13 | "@vitejs/plugin-legacy": "^4.0.2", 14 | "@vitejs/plugin-vue": "^4.1.0", 15 | "@vue/compiler-sfc": "^3.2.30", 16 | "bulma": "^0.9.4", 17 | "histoire": "^0.15.9", 18 | "sass": "^1.32.7", 19 | "vue": "^3.2.47", 20 | "vue-router": "^4.0.12" 21 | }, 22 | "devDependencies": { 23 | "@vitejs/plugin-vue": "^4.1.0", 24 | "eslint": "^8.27.0", 25 | "eslint-config-prettier": "^8.5.0", 26 | "eslint-plugin-import": "^2.25.3", 27 | "eslint-plugin-node": "^11.1.0", 28 | "eslint-plugin-prettier": "^4.0.0", 29 | "eslint-plugin-promise": "^6.0.0", 30 | "eslint-plugin-vue": "^9.3.0", 31 | "vite": "^4.3.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | dist: release 2 | release: 3 | github: 4 | owner: enbility 5 | name: devices-app 6 | 7 | builds: 8 | - id: devices-app 9 | main: main.go 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - amd64 16 | - arm 17 | - arm64 18 | goarm: 19 | - "6" 20 | ignore: 21 | - goos: windows 22 | goarch: arm 23 | - goos: windows 24 | goarch: arm64 25 | 26 | archives: 27 | - builds: 28 | - devices-app 29 | format: tar.gz 30 | format_overrides: 31 | - goos: windows 32 | format: zip 33 | files: 34 | - LICENSE 35 | - README.md 36 | name_template: >- 37 | {{ .ProjectName }}_{{ .Version }}_{{ if eq .Os "darwin" }}macOS{{ else }}{{ .Os }}{{ end }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }} 38 | 39 | universal_binaries: 40 | - replace: true 41 | 42 | checksum: 43 | name_template: "checksums.txt" 44 | 45 | snapshot: 46 | name_template: "{{.ShortCommit}}" 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/enbility/devices-app 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/enbility/eebus-go v0.7.0 7 | github.com/enbility/ship-go v0.6.0 8 | github.com/enbility/spine-go v0.7.0 9 | github.com/gorilla/websocket v1.5.3 10 | ) 11 | 12 | require ( 13 | github.com/ahmetb/go-linq/v3 v3.2.0 // indirect 14 | github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a // indirect 15 | github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 // indirect 16 | github.com/godbus/dbus/v5 v5.1.0 // indirect 17 | github.com/golanguzb70/lrucache v1.2.0 // indirect 18 | github.com/miekg/dns v1.1.62 // indirect 19 | github.com/rickb777/date v1.21.1 // indirect 20 | github.com/rickb777/plural v1.4.2 // indirect 21 | gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect 22 | golang.org/x/mod v0.21.0 // indirect 23 | golang.org/x/net v0.29.0 // indirect 24 | golang.org/x/sync v0.8.0 // indirect 25 | golang.org/x/sys v0.25.0 // indirect 26 | golang.org/x/tools v0.25.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (c) 2022 Andreas Linde & Timo Vogel 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 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_footer.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Fresh Footer 3 | ========================================================================== */ 4 | 5 | .footer { 6 | padding-top: 1rem !important; 7 | padding-bottom: 2rem !important; 8 | } 9 | 10 | footer.footer-dark { 11 | background: $blue-grey; 12 | color: $white; 13 | .columns { 14 | margin-top: 35px; 15 | } 16 | .footer-logo { 17 | img { 18 | height: 40px; 19 | } 20 | } 21 | .footer-column { 22 | .footer-header h3 { 23 | font-weight:500; 24 | font-size: 1.2rem; 25 | text-transform: uppercase; 26 | letter-spacing: 1px; 27 | margin-bottom: 20px; 28 | } 29 | ul.link-list { 30 | line-height: 40px; 31 | font-size: 1.1rem; 32 | a { 33 | color: $light-blue-grey; 34 | font-weight: 400; 35 | transition: all 0.5s; 36 | } 37 | :hover { 38 | color: $smoke-white; 39 | } 40 | } 41 | .level-item .icon { 42 | color: $secondary; 43 | transition: all 0.5s; 44 | :hover { 45 | color: $smoke-white; 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_forms.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Inputs styles 3 | ========================================================================== */ 4 | 5 | input.input { 6 | color: $basaltic-grey; 7 | box-shadow: none !important; 8 | transition: all 0.8s; 9 | padding-bottom: 3px; 10 | &.is-small { 11 | padding-bottom: 2px; 12 | padding-left: 10px; 13 | } 14 | &.is-medium { 15 | padding-bottom: 5px; 16 | } 17 | &.is-large { 18 | padding-bottom: 7px; 19 | } 20 | &:focus, &:active { 21 | border-color: $light-grey; 22 | } 23 | &.rounded { 24 | border-radius: 100px; 25 | } 26 | &.is-primary-focus:focus { 27 | border-color: $primary; 28 | ~ span.icon i { 29 | color: $primary; 30 | } 31 | } 32 | &.is-secondary-focus:focus { 33 | border-color: $secondary; 34 | ~ span.icon i { 35 | color: $secondary; 36 | } 37 | } 38 | &.is-accent-focus:focus { 39 | border-color: $accent; 40 | ~ span.icon i { 41 | color: $accent; 42 | } 43 | } 44 | &.is-bloody-focus:focus { 45 | border-color: $bloody; 46 | ~ span.icon i { 47 | color: $bloody; 48 | } 49 | } 50 | } 51 | 52 | .form-footer { 53 | width: 100%; 54 | } -------------------------------------------------------------------------------- /app/types.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | shipapi "github.com/enbility/ship-go/api" 5 | ) 6 | 7 | type ServiceItem struct { 8 | Ski string `json:"ski"` // the services ski 9 | Trusted bool `json:"trusted"` // if the service is already trusted 10 | State shipapi.ConnectionState `json:"state"` // the connection state 11 | StateError string `json:"error"` // the connection error message if in error state 12 | Brand string `json:"brand"` // the services brand string 13 | Model string `json:"model"` // the services model string 14 | Itself bool `json:"itself"` // Defines if this entry is about this service itself 15 | Discovery string `json:"discovery"` // The SPINE json string 16 | UseCase string `json:"usecase"` // The SPINE json string 17 | } 18 | 19 | type Message struct { 20 | Name MessageName `json:"name"` // The message type 21 | Ski string `json:"ski"` // The SKI the message is meant for, if applicable 22 | Services []*ServiceItem `json:"services"` // The services list, if applicable 23 | // Service ServiceItem `json:"service"` // The service item, if applicable 24 | Enable bool `json:"enable"` // Used with MessageNameAllowRemote 25 | } 26 | 27 | type MessageName string 28 | 29 | const ( 30 | MessageNameService MessageName = "service" // Request or send specific ServiceItem, requires SKI or service 31 | MessageNameServicesList MessageName = "serviceslist" // Request or send ServicesList, requires Services 32 | MessageNamePair MessageName = "pair" // Request or send pairing request, requires SKI 33 | MessageNameAbort MessageName = "abort" // Abort or deny the pairing process 34 | MessageNameUnpair MessageName = "unpair" // Request unpairing request, requires SKI 35 | MessageNameDiscovery MessageName = "discovery" // Send discovery data 36 | MessageNameUsecase MessageName = "usecase" // Send usecase data 37 | MessageNameAllowRemote MessageName = "allowremote" // Enable/Disable allowing remote connections for trust 38 | ) 39 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_responsive.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Responsive Styles 3 | ========================================================================== */ 4 | 5 | @media (max-width: 767px) { 6 | 7 | .landing-caption { 8 | text-align: center; 9 | } 10 | .navbar-menu { 11 | .is-static { 12 | position: absolute; 13 | width: 100%; 14 | } 15 | .is-fixed { 16 | position: fixed; 17 | width: 100%; 18 | } 19 | .navbar-item { 20 | text-align: center !important; 21 | .signup-button { 22 | width: 100% !important; 23 | } 24 | } 25 | .navbar-link { 26 | padding: 10px 20px !important; 27 | } 28 | } 29 | .title.section-title { 30 | font-size: 2rem !important; 31 | } 32 | .level-left.level-social { 33 | display: flex; 34 | justify-content: flex-start; 35 | } 36 | .pushed-image { 37 | margin-top: 0 !important; 38 | } 39 | .testimonial { 40 | margin: 0 auto; 41 | } 42 | } 43 | 44 | 45 | @media only screen and (min-device-width : 768px) and (max-device-width : 1024px) and (orientation : portrait) { 46 | .landing-caption { 47 | text-align: center; 48 | } 49 | .navbar-menu { 50 | .is-static { 51 | position: absolute; 52 | width: 100%; 53 | } 54 | .is-fixed { 55 | position: fixed; 56 | width: 100%; 57 | } 58 | .navbar-item { 59 | text-align: center !important; 60 | .signup-button { 61 | width: 100% !important; 62 | } 63 | } 64 | .navbar-link { 65 | padding: 10px 20px !important; 66 | } 67 | } 68 | .pushed-image { 69 | margin-top: 0 !important; 70 | } 71 | .testimonial { 72 | margin: 0 auto; 73 | } 74 | .is-centered-tablet-portrait { 75 | text-align: center !important; 76 | .divider { 77 | margin: 0 auto !important; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_blogteaser.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Blogteaser Styles 3 | ========================================================================== */ 4 | 5 | .blogteaser { 6 | position: relative; 7 | overflow: hidden; 8 | margin: 10px auto; 9 | min-width: 220px; 10 | max-width: 310px; 11 | width: 100%; 12 | color: #333; 13 | text-align: left; 14 | box-shadow: none !important; 15 | * { 16 | -webkit-box-sizing: border-box; 17 | box-sizing: border-box; 18 | } 19 | img { 20 | max-width: 100%; 21 | height: 80px; 22 | width: 80px; 23 | border-radius: 50%; 24 | margin-right: 5px; 25 | display: block; 26 | z-index: 1; 27 | position: absolute; 28 | right: 60%; 29 | } 30 | blockquote { 31 | margin: 0; 32 | display: block; 33 | border-radius: 8px; 34 | position: relative; 35 | background-color: $smoke-white; 36 | padding: 30px 50px 65px 50px; 37 | font-size: 1.2rem; 38 | font-weight: 500; 39 | margin: 0 0 -40px; 40 | line-height: 1.6em; 41 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.15); 42 | } 43 | blockquote:before, blockquote:after { 44 | font-family: 'FontAwesome'; 45 | // content: "\f10d"; 46 | position: absolute; 47 | font-size: 20px; 48 | opacity: 0.3; 49 | font-style: normal; 50 | } 51 | blockquote:before { 52 | top: 35px; 53 | left: 20px; 54 | } 55 | blockquote:after { 56 | // content: "\f10e"; 57 | right: 20px; 58 | bottom: 35px; 59 | } 60 | .author { 61 | margin: 0; 62 | height: 80px; 63 | display: block; 64 | text-align: left; 65 | color: $white; 66 | padding: 0 35px; 67 | position: relative; 68 | z-index: 0; 69 | h5, span { 70 | left: 50%; 71 | position: absolute; 72 | opacity: 0.8; 73 | padding: 3px 5px; 74 | } 75 | h5 { 76 | text-transform: capitalize; 77 | bottom: 60%; 78 | margin: 0; 79 | font-weight: 600; 80 | font-size: 1.2rem; 81 | color: $blue-grey; 82 | } 83 | span { 84 | font-size: 0.8em; 85 | color: $white; 86 | top: 50%; 87 | } 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Desktop App 2 | 3 | This app demonstrates the GUI supported pairing process using the [eebus-go library](https://github.com/enbility/eebus-go). It consists of a server component written in Go and a web implemented using VueJS 3. 4 | 5 | Once this app is paired with another service (could also be the same service running on a different port), it will show the SPINE data with details about supported usecases and features. The goal is to also present this information in a more user friendly way in the future. 6 | 7 | Another goal is to provide an executable for every supported platform that contains everything required. 8 | 9 | The service requires a certificate and a key which will be created automatically and saved in the working folder if file names are not provided or the default filenames are not found. 10 | 11 | ## First steps 12 | 13 | - Download and install [golang](https://go.dev) for your computer 14 | - Download and install [NodeJS and NPM](https://nodejs.org/) if you do not already have it 15 | - Download the source code of this repository 16 | - Run `npm install` inside the root repository folder 17 | - Now follow either the `Development` or `Build binary` steps 18 | 19 | ## Development 20 | 21 | ### Running the server component 22 | 23 | - `make ui` for creating the UI assets 24 | - `go run main.go -h` to see all the possible parameters. 25 | - `go run main.go` to start with the default parameters. 26 | 27 | ### Running the web frontend 28 | 29 | `npx vite dev` to start with the default parameters using `vite.config.js`. The web service is now accessible at `http://localhost:7051/` 30 | 31 | ## Build binary 32 | 33 | - `make ui` for creating the UI assets 34 | - `make build` for building the binary for the local system 35 | - execute the binary with `./devices-app` 36 | - Open the website in a browser at `http://localhost:7050/` 37 | 38 | ## Usage 39 | 40 | ```sh 41 | General Usage: 42 | devices-app 43 | Optional port for the HTTPD server 44 | Optional port for the EEBUS service 45 | Optional filepath for the cert file 46 | Option filepath for the key file 47 | Option mDNS serial string 48 | 49 | Default values: 50 | httpd-port: 7050 51 | eebus-port: 4815 52 | crt-file: cert.crt (same folder as executable) 53 | key-file: cert.key (same folder as executable) 54 | serial: 123456789 55 | 56 | If no cert-file or key-file parameters are provided and 57 | the files do not exist, they will be created automatically. 58 | ``` 59 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_testimonials.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Testimonials Styles 3 | ========================================================================== */ 4 | 5 | .testimonial { 6 | position: relative; 7 | overflow: hidden; 8 | margin: 10px auto; 9 | min-width: 220px; 10 | max-width: 310px; 11 | width: 100%; 12 | color: #333; 13 | text-align: left; 14 | box-shadow: none !important; 15 | * { 16 | -webkit-box-sizing: border-box; 17 | box-sizing: border-box; 18 | } 19 | img { 20 | max-width: 100%; 21 | height: 80px; 22 | width: 80px; 23 | border-radius: 50%; 24 | margin-right: 5px; 25 | display: block; 26 | z-index: 1; 27 | position: absolute; 28 | right: 60%; 29 | } 30 | blockquote { 31 | margin: 0; 32 | display: block; 33 | border-radius: 8px; 34 | position: relative; 35 | background-color: $smoke-white; 36 | padding: 30px 50px 65px 50px; 37 | font-size: 1.2rem; 38 | font-weight: 500; 39 | margin: 0 0 -40px; 40 | line-height: 1.6em; 41 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.15); 42 | } 43 | blockquote:before, blockquote:after { 44 | font-family: 'FontAwesome'; 45 | content: "\f10d"; 46 | position: absolute; 47 | font-size: 20px; 48 | opacity: 0.3; 49 | font-style: normal; 50 | } 51 | blockquote:before { 52 | top: 35px; 53 | left: 20px; 54 | } 55 | blockquote:after { 56 | content: "\f10e"; 57 | right: 20px; 58 | bottom: 35px; 59 | } 60 | .author { 61 | margin: 0; 62 | height: 80px; 63 | display: block; 64 | text-align: left; 65 | color: $white; 66 | padding: 0 35px; 67 | position: relative; 68 | z-index: 0; 69 | h5, span { 70 | left: 50%; 71 | position: absolute; 72 | opacity: 0.8; 73 | padding: 3px 5px; 74 | } 75 | h5 { 76 | text-transform: capitalize; 77 | bottom: 60%; 78 | margin: 0; 79 | font-weight: 600; 80 | font-size: 1.2rem; 81 | color: $blue-grey; 82 | } 83 | span { 84 | font-size: 0.8em; 85 | color: $white; 86 | top: 50%; 87 | } 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /app/connection.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | const ( 12 | writeWait = 10 * time.Second 13 | maxMessageSize = 8192 14 | pongWait = 60 * time.Second 15 | pingPeriod = (pongWait * 9) / 10 16 | closeGracePeriod = 15 * time.Second 17 | ) 18 | 19 | type Connection struct { 20 | cem *Cem 21 | 22 | conn *websocket.Conn 23 | 24 | sendChannel chan []byte 25 | closeChannel chan struct{} 26 | } 27 | 28 | func NewConnection(cem *Cem, ws *websocket.Conn) *Connection { 29 | conn := &Connection{ 30 | cem: cem, 31 | conn: ws, 32 | sendChannel: make(chan []byte, 1), 33 | closeChannel: make(chan struct{}, 1), 34 | } 35 | 36 | go conn.readPump(ws) 37 | go conn.writePump(ws) 38 | 39 | return conn 40 | } 41 | 42 | func (c *Connection) sendMessage(msg Message) { 43 | message, err := json.Marshal(msg) 44 | if err != nil { 45 | log.Println("Error json marshal:", err) 46 | return 47 | } 48 | 49 | c.sendChannel <- message 50 | } 51 | 52 | func (c *Connection) readPump(ws *websocket.Conn) { 53 | defer func() { 54 | c.cem.RemoveConnection(c) 55 | c.conn.Close() 56 | }() 57 | 58 | _ = ws.SetReadDeadline(time.Now().Add(pongWait)) 59 | ws.SetPongHandler(func(string) error { _ = ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 60 | 61 | for { 62 | _, message, err := ws.ReadMessage() 63 | if err != nil { 64 | break 65 | } 66 | log.Println("Received", string(message)) 67 | 68 | c.cem.handleMessage(c, message) 69 | } 70 | } 71 | 72 | func (c *Connection) writePump(ws *websocket.Conn) { 73 | ticker := time.NewTicker(pingPeriod) 74 | defer func() { 75 | ticker.Stop() 76 | c.conn.Close() 77 | }() 78 | 79 | for { 80 | select { 81 | case <-c.closeChannel: 82 | return 83 | case message, ok := <-c.sendChannel: 84 | _ = ws.SetWriteDeadline(time.Now().Add(writeWait)) 85 | if !ok { 86 | _ = ws.WriteMessage(websocket.CloseMessage, []byte{}) 87 | return 88 | } 89 | 90 | if err := ws.WriteMessage(websocket.TextMessage, message); err != nil { 91 | log.Println("Error sending:", err) 92 | ws.Close() 93 | return 94 | } 95 | 96 | log.Println("Sent: ", string(message)) 97 | case <-ticker.C: 98 | _ = ws.SetWriteDeadline(time.Now().Add(writeWait)) 99 | if err := ws.WriteMessage(websocket.PingMessage, nil); err != nil { 100 | log.Println("Error sending:", err) 101 | return 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Dropdown styles 3 | ========================================================================== */ 4 | 5 | // Hover Dropdowns 6 | div.nav-item.is-drop a { 7 | padding-right: 7px; 8 | } 9 | 10 | div.nav-item.is-drop:hover .dropContain .dropOut { 11 | opacity: 1; 12 | } 13 | 14 | div.nav-item.is-drop:hover, div.nav-item.is-drop:hover a, { 15 | border-bottom: 1px solid transparent !important; 16 | color: $secondary; 17 | } 18 | 19 | div.nav-item.is-drop:hover .dropContain { 20 | top: 65px; 21 | animation: fadeInUp 0.27s ease-out; 22 | } 23 | 24 | span.drop-caret { 25 | position: relative; 26 | top: 5px; 27 | } 28 | 29 | div.nav-item.is-drop { 30 | position: relative; 31 | .dropContain { 32 | width: 220px; 33 | position: absolute; 34 | z-index: 3; 35 | left: 50%; 36 | margin-left: -110px; /* half of width */ 37 | top: -400px; 38 | .dropOut { 39 | width: 220px; 40 | background: $white; 41 | float: left; 42 | position: relative; 43 | margin-top: 15px; 44 | opacity: 0; 45 | -webkit-border-radius: 4px; 46 | -moz-border-radius: 4px; 47 | border-radius: 4px; 48 | -webkit-box-shadow: 0 1px 6px rgba(0,0,0,.15); 49 | -moz-box-shadow: 0 1px 6px rgba(0,0,0,.15); 50 | box-shadow: 0 1px 6px rgba(0,0,0,.15); 51 | -webkit-transition: all .5s ease-out; 52 | -moz-transition: all .5s ease-out; 53 | -ms-transition: all .5s ease-out; 54 | -o-transition: all .5s ease-out; 55 | transition: all .5s ease-out; 56 | } 57 | .dropOut .triangle { 58 | width: 0; 59 | height: 0; 60 | position: absolute; 61 | border-left: 8px solid transparent; 62 | border-right: 8px solid transparent; 63 | border-bottom: 8px solid $white; 64 | top: -8px; 65 | left: 50%; 66 | margin-left: -8px; 67 | } 68 | .dropOut ul li { 69 | text-align: left; 70 | float: left; 71 | width: 200px; 72 | padding: 12px 0 10px 15px; 73 | margin: 0px 10px; 74 | color: #777; 75 | -webkit-border-radius: 4px; 76 | -moz-border-radius: 4px; 77 | border-radius: 4px; 78 | -webkit-transition: background .1s ease-out; 79 | -moz-transition: background .1s ease-out; 80 | -ms-transition: background .1s ease-out; 81 | -o-transition: background .1s ease-out; 82 | transition: background .1s ease-out; 83 | &:hover { 84 | background: $light-grey; 85 | cursor: pointer; 86 | } 87 | } 88 | .dropOut ul { 89 | float: left; 90 | padding: 10px 0; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_animations.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | General Keyframes animations 3 | ========================================================================== */ 4 | 5 | .animated { 6 | animation-duration: 0.5s; 7 | animation-fill-mode: both; 8 | -webkit-animation-duration: 0.5s; 9 | -webkit-animation-fill-mode: both; 10 | } 11 | 12 | //Delays 13 | .delay-1 { 14 | animation-delay: .25s; 15 | } 16 | .delay-2 { 17 | animation-delay: .5s; 18 | } 19 | .delay-3 { 20 | animation-delay: .75s; 21 | } 22 | .delay-4 { 23 | animation-delay: 1s; 24 | } 25 | 26 | // FADE IN LEFT 27 | @keyframes fadeInLeft { 28 | from { 29 | -webkit-transform: translate3d(20px, 0, 0); 30 | transform: translate3d(20px, 0, 0); 31 | opacity: 0; 32 | } 33 | to { 34 | -webkit-transform: translate3d(0, 0, 0); 35 | transform: translate3d(0, 0, 0); 36 | opacity: 1; 37 | } 38 | } 39 | @-webkit-keyframes fadeInLeft { 40 | from { 41 | -webkit-transform: translate3d(20px, 0, 0); 42 | transform: translate3d(20px, 0, 0); 43 | opacity: 0; 44 | } 45 | to { 46 | -webkit-transform: translate3d(0, 0, 0); 47 | transform: translate3d(0, 0, 0); 48 | opacity: 1; 49 | } 50 | } 51 | 52 | .preFadeInLeft { 53 | opacity: 0; 54 | } 55 | 56 | .fadeInLeft { 57 | opacity: 0; 58 | animation-name: fadeInLeft; 59 | -webkit-animation-name: fadeInLeft; 60 | } 61 | 62 | // FADE IN UP 63 | @keyframes fadeInUp { 64 | from { 65 | -webkit-transform: translate3d(0, 20px, 0); 66 | transform: translate3d(0, 20px, 0); 67 | } 68 | to { 69 | -webkit-transform: translate3d(0, 0, 0); 70 | transform: translate3d(0, 0, 0); 71 | opacity: 1; 72 | } 73 | } 74 | @-webkit-keyframes fadeInUp { 75 | from { 76 | -webkit-transform: translate3d(0, 20px, 0); 77 | transform: translate3d(0, 20px, 0); 78 | } 79 | to { 80 | -webkit-transform: translate3d(0, 0, 0); 81 | transform: translate3d(0, 0, 0); 82 | opacity: 1; 83 | } 84 | } 85 | .preFadeInUp { 86 | opacity: 0; 87 | } 88 | .fadeInUp { 89 | opacity: 0; 90 | animation-name: fadeInUp; 91 | -webkit-animation-name: fadeInUp; 92 | } 93 | 94 | //Gelatine 95 | .gelatine { 96 | animation: gelatine 0.6s; 97 | animation-duration: 0.6s; 98 | -webkit-animation-duration: 0.5s; 99 | animation-fill-mode: both; 100 | -webkit-animation-fill-mode: both; 101 | } 102 | 103 | @keyframes gelatine { 104 | from, to { transform: scale(1, 1); } 105 | 25% { transform: scale(0.9, 1.1); } 106 | 50% { transform: scale(1.1, 0.9); } 107 | 75% { transform: scale(0.95, 1.05); } 108 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "flag" 6 | "fmt" 7 | "io/fs" 8 | "log" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "strconv" 13 | "syscall" 14 | 15 | "github.com/enbility/devices-app/app" 16 | "github.com/gorilla/websocket" 17 | ) 18 | 19 | //go:embed dist 20 | var web embed.FS 21 | 22 | const ( 23 | httpdPort int = 7050 24 | ) 25 | 26 | var upgrader = websocket.Upgrader{ 27 | ReadBufferSize: 1024, 28 | WriteBufferSize: 1024, 29 | CheckOrigin: func(r *http.Request) bool { 30 | // allow connection from any host 31 | return true 32 | }, 33 | } 34 | 35 | func serveWs(cem *app.Cem, w http.ResponseWriter, r *http.Request) { 36 | ws, err := upgrader.Upgrade(w, r, nil) 37 | if err != nil { 38 | log.Println("upgrade error:", err) 39 | return 40 | } 41 | 42 | conn := app.NewConnection(cem, ws) 43 | cem.AddConnection(conn) 44 | } 45 | 46 | func usage() { 47 | fmt.Println("General Usage:") 48 | fmt.Println(" devices-app ") 49 | fmt.Println(" Optional port for the HTTPD server") 50 | fmt.Println(" Optional port for the EEBUS service") 51 | fmt.Println(" Optional filepath for the cert file") 52 | fmt.Println(" Option filepath for the key file") 53 | fmt.Println(" Option mDNS serial string") 54 | fmt.Println() 55 | fmt.Println("Default values:") 56 | fmt.Println(" httpd-port:", httpdPort) 57 | fmt.Println(" eebus-port: 4815") 58 | fmt.Println(" crt-file: cert.crt (same folder as executable)") 59 | fmt.Println(" key-file: cert.key (same folder as executable)") 60 | fmt.Println(" serial: 123456789") 61 | fmt.Println() 62 | fmt.Println("If no cert-file or key-file parameters are provided and") 63 | fmt.Println("the files do not exist, they will be created automatically.") 64 | } 65 | 66 | func main() { 67 | if len(os.Args) == 2 && os.Args[1] == "-h" { 68 | usage() 69 | return 70 | } 71 | 72 | portHttpd := httpdPort 73 | if len(os.Args) > 1 { 74 | if tempPort, err := strconv.Atoi(os.Args[1]); err == nil { 75 | portHttpd = tempPort 76 | } 77 | } 78 | log.Println("Web Server running at port", portHttpd) 79 | 80 | hems := app.NewHems() 81 | hems.Run() 82 | 83 | serverRoot, err := fs.Sub(web, "dist") 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | http.Handle("/", http.FileServer(http.FS(serverRoot))) 88 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 89 | serveWs(hems, w, r) 90 | }) 91 | go func() { 92 | host := fmt.Sprintf("0.0.0.0:%d", portHttpd) 93 | addr := flag.String("addr", host, "http service address") 94 | 95 | if err := http.ListenAndServe(*addr, nil); err != nil { 96 | log.Fatal(err) 97 | } 98 | }() 99 | 100 | // Clean exit to make sure mdns shutdown is invoked 101 | sig := make(chan os.Signal, 1) 102 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 103 | <-sig 104 | // User exit 105 | } 106 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_cards.scss: -------------------------------------------------------------------------------- 1 | /*! _cards.scss v1.0.0 | Commercial License | built on top of bulma.io/Bulmax */ 2 | 3 | /* ========================================================================== 4 | Cards and Card content styles 5 | ========================================================================== */ 6 | 7 | // Feature Card 8 | .feature-card { 9 | width: 300px; 10 | height: 400px; 11 | background-color: #fff; 12 | border-radius: 3px; 13 | margin: 0 auto; 14 | .card-title h4 { 15 | font-family: 'Open Sans', sans-serif; 16 | padding-top: 25px; 17 | font-size: 1.2rem; 18 | font-weight: 600; 19 | color: $blue-grey; 20 | } 21 | .card-subtitle h3 { 22 | font-size: 1.0rem; 23 | font-weight: 400; 24 | } 25 | // .card-icon img { 26 | // height: 120px; 27 | // margin-top: 20px; 28 | // } 29 | .card-icon img { 30 | max-width: 100%; 31 | height: 80px; 32 | width: 80px; 33 | border-radius: 50%; 34 | margin-top: 20px; 35 | margin-bottom: -20px 36 | // margin-right: 5px; 37 | // display: block; 38 | // z-index: 1; 39 | // position: absolute; 40 | // right: 70%; 41 | } 42 | .card-text { 43 | margin-top: 20px; 44 | padding: 0 40px; 45 | color: $muted-grey; 46 | ul { 47 | li { 48 | padding-bottom: 10px; 49 | } 50 | } 51 | } 52 | .card-action { 53 | margin-top: 20px; 54 | } 55 | &.is-bordered { 56 | border: 1px solid $fade-grey; 57 | } 58 | } 59 | 60 | // Flex Card 61 | .flex-card { 62 | position: relative; 63 | background-color: #fff; 64 | border: 0; 65 | border-radius: 0.1875rem; 66 | display: inline-block; 67 | position: relative; 68 | overflow: hidden; 69 | width: 100%; 70 | margin-bottom: 20px; 71 | &.raised { 72 | box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.2); 73 | } 74 | .tabs { 75 | padding: 15px 0.7rem; 76 | } 77 | .navtab-content { 78 | min-height: 190px; 79 | p { 80 | padding: 0 0.8rem 20px; 81 | } 82 | } 83 | .navigation-tabs { 84 | &.outlined-pills .tabs.tabs-header { 85 | &.primary { 86 | background-color: $primary; 87 | } 88 | &.secondary { 89 | background-color: $secondary; 90 | } 91 | &.accent { 92 | background-color: $accent; 93 | } 94 | ul li a { 95 | color: $grey-white; 96 | } 97 | ul li.is-active a { 98 | color: $white; 99 | border: 1px solid $white; 100 | border-bottom-color: $white !important; 101 | } 102 | } 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_sections.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Section Styles 3 | ========================================================================== */ 4 | 5 | //Sections 6 | .section { 7 | padding-bottom: 2rem !important; 8 | 9 | &.section-light-grey { 10 | background-color: $light-grey; 11 | } 12 | &.section-feature-grey { 13 | background-color: $section-grey; 14 | } 15 | &.section-secondary { 16 | background-color: $secondary; 17 | } 18 | &.section-blue-grey { 19 | background-color: $blue-grey; 20 | } 21 | &.section-half { 22 | height: 75vh !important; 23 | } 24 | .title, .subtitle { 25 | font-family: 'Open Sans', sans-serif; 26 | 27 | } 28 | .subtitle { 29 | &.is-muted { 30 | color: $muted-grey; 31 | } 32 | } 33 | 34 | .content .svg-250 { 35 | max-width: 250px !important; 36 | height: auto; 37 | } 38 | } 39 | 40 | //Titles 41 | .title-wrapper { 42 | max-width: 500px; 43 | margin: 0 auto; 44 | .title, .subtitle { 45 | font-family: 'Open Sans', sans-serif; 46 | 47 | } 48 | .subtitle { 49 | &.is-muted { 50 | color: $muted-grey; 51 | } 52 | } 53 | } 54 | 55 | //Divider 56 | .divider { 57 | height: 3px; 58 | border-radius: 50px; 59 | background: $secondary; 60 | width: 60px; 61 | &.is-centered { 62 | margin: 0 auto; 63 | } 64 | } 65 | 66 | //Wrapper 67 | .content-wrapper { 68 | padding: 60px 0; 69 | } 70 | 71 | 72 | //Pulled image 73 | img.pushed-image { 74 | margin-top: -5vh; 75 | } 76 | 77 | //Icon box 78 | .media.icon-box { 79 | border-top: none !important; 80 | .media-content .content { 81 | span { 82 | display: block; 83 | } 84 | .icon-box-title { 85 | color: $blue-grey; 86 | font-size: 1.2rem; 87 | font-weight: 600; 88 | } 89 | .icon-box-text { 90 | color: $title-grey; 91 | font-size: 1rem; 92 | font-weight: 400; 93 | } 94 | } 95 | } 96 | 97 | //List box 98 | .media.list-box { 99 | border-top: none !important; 100 | .media-content .content { 101 | div { 102 | display: block; 103 | } 104 | .list-box-title { 105 | color: $blue-grey; 106 | font-size: 1.6rem; 107 | font-weight: 600; 108 | } 109 | .list-box-subtitle { 110 | color: $blue-grey; 111 | font-size: 1.2rem; 112 | font-weight: 400; 113 | } 114 | .list-box-text { 115 | color: $medium-grey; 116 | font-size: 1rem; 117 | font-weight: 400; 118 | } 119 | 120 | .list-box-text a { 121 | color: $dark-grey; 122 | font-weight: 600; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_buttons.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Classes to change the feel of bulma buttons 3 | ========================================================================== */ 4 | 5 | // CTA buttons 6 | 7 | .button { 8 | cursor: pointer; 9 | transition: all 0.5s; 10 | &.cta { 11 | font-family: 'Open Sans', sans-serif; 12 | font-size: 1rem; 13 | font-weight: 600; 14 | padding: 26px 40px 26px 40px; 15 | } 16 | &.is-clear { 17 | line-height: 0 !important; 18 | } 19 | &.rounded { 20 | border-radius: 500px; 21 | } 22 | &.raised:hover { 23 | box-shadow: 0 14px 26px -12px rgba(0, 0, 0, 0.42), 0 4px 23px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2) !important; 24 | opacity: 0.8; 25 | } 26 | &.btn-outlined { 27 | background: transparent; 28 | } 29 | &.signup-button { 30 | font-size: .9rem; 31 | font-weight: 600; 32 | font-family: 'Open Sans', sans-serif; 33 | padding: 24px 26px; 34 | width: 130px; 35 | } 36 | } 37 | 38 | .button { 39 | &.primary-btn { 40 | outline: none; 41 | border-color: $primary; 42 | background-color: $primary; 43 | color: $white; 44 | transition: all 0.5s; 45 | &:hover { 46 | color: $white; 47 | } 48 | &.raised:hover { 49 | box-shadow: 0 14px 26px -12px rgba(79, 193, 234, 0.42), 0 4px 23px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(79, 193, 234, 0.2) !important; 50 | opacity: 0.8; 51 | } 52 | &.btn-outlined { 53 | border-color: $primary; 54 | color: $primary; 55 | background-color: transparent; 56 | &:hover { 57 | color: $white; 58 | background-color: $primary; 59 | } 60 | } 61 | } 62 | &.secondary-btn { 63 | outline: none; 64 | border-color: $secondary; 65 | background-color: $secondary; 66 | color: $white; 67 | transition: all 0.5s; 68 | &:hover { 69 | color: $white; 70 | } 71 | &.raised:hover { 72 | box-shadow: 0 14px 26px -12px rgba(243, 146, 0, 0.42), 0 4px 23px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(243, 146, 0, 0.2) !important; 73 | opacity: 0.8; 74 | } 75 | &.btn-outlined { 76 | border-color: $secondary; 77 | color: $secondary; 78 | background-color: transparent; 79 | &:hover { 80 | color: $white; 81 | background-color: $secondary; 82 | } 83 | } 84 | } 85 | &.button.accent-btn { 86 | outline: none; 87 | border-color: $accent; 88 | background-color: $accent; 89 | color: $white; 90 | transition: all 0.5s; 91 | &:hover { 92 | color: $white; 93 | } 94 | &.raised:hover { 95 | box-shadow: 0 14px 26px -12px rgba(104, 187, 136, 0.42), 0 4px 23px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(104, 187, 136, 0.2) !important; 96 | opacity: 0.8; 97 | } 98 | &.btn-outlined { 99 | border-color: $accent; 100 | color: $accent; 101 | background-color: transparent; 102 | &:hover { 103 | color: $white; 104 | background-color: $accent; 105 | } 106 | } 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_navbar.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Navbar 3 | ========================================================================== */ 4 | 5 | //Navbar 6 | .navbar.is-fresh { 7 | background: $blue-grey; 8 | position: relative; 9 | min-height: 3.8rem; 10 | transition: all .3s; 11 | z-index: 99; 12 | .container { 13 | min-height: 4rem; 14 | } 15 | &.no-shadow { 16 | box-shadow: none !important; 17 | } 18 | //Responsive menu icon 19 | .navbar-burger { 20 | color: $accent; 21 | width: 4rem; 22 | height: 4rem; 23 | } 24 | //Brand 25 | .navbar-brand { 26 | min-height: 4rem; 27 | img { 28 | max-height: 36px !important; 29 | height: 36px; 30 | } 31 | //Removing navbar item default hover behaviour 32 | &:hover { 33 | .navbar-item { 34 | background: transparent !important; 35 | } 36 | } 37 | } 38 | .navbar-end { 39 | background-color: $blue-grey; 40 | align-items: center; 41 | } 42 | //Navbar menu 43 | .navbar-menu { 44 | background-color: $blue-grey !important; 45 | } 46 | //Navbar items 47 | .navbar-item { 48 | color: $white; 49 | // color: $muted-grey; 50 | &.is-secondary { 51 | &:hover { 52 | color: $secondary !important; 53 | background-color: $blue-grey !important; 54 | } 55 | } 56 | &.has-dropdown { 57 | padding: 10px 0; 58 | .navbar-link { 59 | color: $muted-grey; 60 | &:after { 61 | top: 55%; 62 | height: 0.5em; 63 | width: 0.5em; 64 | border-width: 2px; 65 | border-color: $muted-grey; 66 | } 67 | } 68 | .navbar-dropdown { 69 | top: 3.4rem; 70 | min-width: 220px; 71 | margin-top: 4px; 72 | border-top-color: $secondary; 73 | .navbar-item { 74 | padding: 10px 20px; 75 | } 76 | } 77 | &:hover { 78 | .navbar-link { 79 | color: $secondary; 80 | &:after { 81 | border-color: $secondary; 82 | } 83 | } 84 | } 85 | } 86 | .signup { 87 | display: block; 88 | line-height: 0; 89 | font-size: .9rem !important; 90 | } 91 | } 92 | 93 | //Fixed navbar modifier 94 | &.is-fixed { 95 | position: fixed; 96 | top: 0; 97 | left: 0; 98 | width: 100%; 99 | min-height: 4rem !important; 100 | background: $white; 101 | box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.12); 102 | a { 103 | color: $blue-grey; 104 | &:hover { 105 | color: $primary; 106 | } 107 | } 108 | } 109 | } 110 | 111 | //Cloned fixed navbar 112 | #navbar-clone { 113 | position: fixed; 114 | top: 0; 115 | left: 0; 116 | width: 100%; 117 | // background: $white; 118 | background: $blue-grey; 119 | transform: translateY(-100%); 120 | z-index: 100; 121 | box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.12); 122 | &.is-active { 123 | transform: translateY(0); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /frontend/src/styles/fresh/partials/_utils.scss: -------------------------------------------------------------------------------- 1 | // Resets 2 | section:focus { 3 | outline: none !important; 4 | } 5 | 6 | button { 7 | &:focus, &:active { 8 | outline: none; 9 | } 10 | } 11 | 12 | // Preloader 13 | #preloader { 14 | position: fixed; 15 | top: 0; 16 | left: 0; 17 | right: 0; 18 | bottom: 0; 19 | background-color: $white; 20 | z-index: 99; 21 | } 22 | 23 | #status { 24 | width: 200px; 25 | height: 200px; 26 | position: absolute; 27 | left: 50%; 28 | // centers the loading animation horizontally on the screen 29 | top: 50%; 30 | // centers the loading animation vertically on the screen 31 | // background-image: url(../images/loaders/rings.svg); 32 | background-size: 80px 80px; 33 | // path to loading animation 34 | background-repeat: no-repeat; 35 | background-position: center; 36 | margin: -100px 0 0 -100px; 37 | // width and height divided by two 38 | } 39 | 40 | // Back to top button 41 | #backtotop { 42 | position: fixed; 43 | right: 0; 44 | opacity: 0; 45 | visibility: hidden; 46 | bottom: 25px; 47 | margin: 0 25px 0 0; 48 | z-index: 9999; 49 | transition: 0.35s; 50 | transform: scale(0.7); 51 | transition: all 0.5s; 52 | } 53 | 54 | #backtotop.visible { 55 | opacity: 1; 56 | visibility: visible; 57 | transform: scale(1); 58 | 59 | } 60 | 61 | #backtotop.visible a:hover { 62 | outline: none; 63 | opacity: 0.9; 64 | background: $secondary; 65 | } 66 | 67 | #backtotop a { 68 | outline: none; 69 | text-decoration: none; 70 | border: 0 none; 71 | display: block; 72 | width: 46px; 73 | height: 46px; 74 | background-color: $medium-grey; 75 | opacity: 1; 76 | transition: all 0.3s; 77 | border-radius: 50%; 78 | text-align: center; 79 | font-size: 26px 80 | } 81 | 82 | body #backtotop a { 83 | outline: none; 84 | color: #fff; 85 | } 86 | 87 | #backtotop a:after { 88 | outline: none; 89 | content: "\f106"; 90 | font-family: "FontAwesome"; 91 | position: relative; 92 | display: block; 93 | top: 50%; 94 | -webkit-transform: translateY(-55%); 95 | transform: translateY(-55%); 96 | } 97 | 98 | 99 | //Helpers 100 | .is-disabled { 101 | pointer-events: none; 102 | opacity: 0.4; 103 | cursor: default !important; 104 | } 105 | 106 | .is-hidden { 107 | display: none !important; 108 | } 109 | 110 | .stuck { 111 | position:fixed !important; 112 | top: 0 !important; 113 | z-index: 2 !important; 114 | } 115 | 116 | .light-text { 117 | color: $white !important; 118 | } 119 | 120 | .mb-20 { 121 | margin-bottom: 20px; 122 | } 123 | 124 | .mb-40 { 125 | margin-bottom: 40px; 126 | } 127 | 128 | .mb-60 { 129 | margin-bottom: 60px; 130 | } 131 | 132 | .mt-20 { 133 | margin-top: 20px; 134 | } 135 | 136 | .mt-40 { 137 | margin-top: 40px; 138 | } 139 | 140 | .mt-50 { 141 | margin-top: 50px; 142 | } 143 | 144 | .mt-60 { 145 | margin-top: 60px; 146 | } 147 | 148 | .ml-30 { 149 | margin-left: 30px; 150 | } 151 | 152 | .huge-pb { 153 | padding-bottom: 100px; 154 | } 155 | 156 | .pb-20 { 157 | padding-bottom: 20px !important; 158 | } 159 | 160 | .pb-40 { 161 | padding-bottom: 40px !important; 162 | } 163 | 164 | //Input placeholders 165 | ::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 166 | color: $placeholder; 167 | } 168 | ::-moz-placeholder { /* Firefox 19+ */ 169 | color: $placeholder; 170 | } 171 | :-ms-input-placeholder { /* IE 10+ */ 172 | color: $placeholder; 173 | } 174 | :-moz-placeholder { /* Firefox 18- */ 175 | color: $placeholder; 176 | } 177 | 178 | .is-horizontal-center { 179 | justify-content: center; 180 | } 181 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ahmetb/go-linq/v3 v3.2.0 h1:BEuMfp+b59io8g5wYzNoFe9pWPalRklhlhbiU3hYZDE= 2 | github.com/ahmetb/go-linq/v3 v3.2.0/go.mod h1:haQ3JfOeWK8HpVxMtHHEMPVgBKiYyQ+f1/kLZh/cj9U= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/enbility/eebus-go v0.7.0 h1:Uh3i+HMmTYecWA+BBlYYhNFuNtqzWWQarbv4z9n/aQI= 6 | github.com/enbility/eebus-go v0.7.0/go.mod h1:ftoVhXGC00IEcfN4RZSb1PbBIglE9i3JYqwrjhXnYSA= 7 | github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a h1:foChWb8lhzqa6lWDRs6COYMdp649YlUirFP8GqoT0JQ= 8 | github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a/go.mod h1:H64mhYcAQUGUUnVqMdZQf93kPecH4M79xwH95Lddt3U= 9 | github.com/enbility/ship-go v0.6.0 h1:1ft5NJJHqqGU3/ryYwQj8xBYJLFbf0q2cP9mjlYHlgw= 10 | github.com/enbility/ship-go v0.6.0/go.mod h1:JJp8EQcJhUhTpZ2LSEU4rpdaM3E2n08tswWFWtmm/wU= 11 | github.com/enbility/spine-go v0.7.0 h1:UZeghFgnM3VFU0ghc57Htt6gnxwP9jLppfU2GUMJGgY= 12 | github.com/enbility/spine-go v0.7.0/go.mod h1:IF1sBTr7p3wXqlejeBJcJ8BYFlzzRaZcJsGw8XjgEgc= 13 | github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 h1:XOYvxKtT1oxT37w/5oEiRLuPbm9FuJPt3fiYhX0h8Po= 14 | github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6/go.mod h1:BszP9qFV14mPXgyIREbgIdQtWxbAj3OKqvK02HihMoM= 15 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 16 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 17 | github.com/golanguzb70/lrucache v1.2.0 h1:VjpjmB4VTf9VXBtZTJGcgcN0CNFM5egDrrSjkGyQOlg= 18 | github.com/golanguzb70/lrucache v1.2.0/go.mod h1:zc2GD26KwGEDdTHsCCTcJorv/11HyKwQVS9gqg2bizc= 19 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 20 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 21 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 22 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 23 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 24 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 25 | github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= 26 | github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/rickb777/date v1.21.1 h1:tUcQS8riIRoYK5kUAv5aevllFEYUEk2x8OYDyoldOn4= 30 | github.com/rickb777/date v1.21.1/go.mod h1:gnDexsbXViZr2fCKMrY3m6IfAF5U2vSkEaiGJcNFaLQ= 31 | github.com/rickb777/plural v1.4.2 h1:Kl/syFGLFZ5EbuV8c9SVud8s5HI2HpCCtOMw2U1kS+A= 32 | github.com/rickb777/plural v1.4.2/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= 33 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 34 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 35 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 36 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 37 | gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k= 38 | gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI= 39 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 40 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 41 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 42 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 43 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 44 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 45 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 46 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 47 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 48 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 49 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 50 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 51 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 52 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /frontend/src/assets/logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/hems.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/json" 8 | "encoding/pem" 9 | "fmt" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "sort" 14 | "strconv" 15 | "strings" 16 | "sync" 17 | "time" 18 | 19 | "github.com/enbility/eebus-go/api" 20 | "github.com/enbility/eebus-go/service" 21 | shipapi "github.com/enbility/ship-go/api" 22 | "github.com/enbility/ship-go/cert" 23 | "github.com/enbility/spine-go/model" 24 | ) 25 | 26 | const ( 27 | eebusPort int = 4815 28 | ) 29 | 30 | type Cem struct { 31 | mux sync.Mutex 32 | servicesMux sync.Mutex 33 | 34 | eebusService *service.Service 35 | 36 | currentRemoteServices []shipapi.RemoteService 37 | 38 | servicesList []*ServiceItem 39 | 40 | discoveryData map[string]string 41 | usecaseData map[string]string 42 | 43 | connections map[*Connection]bool 44 | 45 | allowRemoteConnections bool 46 | } 47 | 48 | func NewHems() *Cem { 49 | hems := &Cem{ 50 | connections: make(map[*Connection]bool), 51 | servicesList: make([]*ServiceItem, 0), 52 | discoveryData: make(map[string]string), 53 | usecaseData: make(map[string]string), 54 | allowRemoteConnections: true, 55 | } 56 | 57 | return hems 58 | } 59 | 60 | func (c *Cem) createCertificate(certPath, keyPath string) (tls.Certificate, error) { 61 | certificate, err := cert.CreateCertificate("Demo", "Demo", "DE", "Demo-Unit-01") 62 | if err != nil { 63 | return tls.Certificate{}, err 64 | } 65 | 66 | pemdata := pem.EncodeToMemory(&pem.Block{ 67 | Type: "CERTIFICATE", 68 | Bytes: certificate.Certificate[0], 69 | }) 70 | if err := os.WriteFile(certPath, pemdata, 0666); err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | b, err := x509.MarshalECPrivateKey(certificate.PrivateKey.(*ecdsa.PrivateKey)) 75 | if err != nil { 76 | return tls.Certificate{}, err 77 | } 78 | pemdata = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) 79 | if err := os.WriteFile(keyPath, pemdata, 0666); err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | return certificate, nil 84 | } 85 | 86 | func (c *Cem) Run() { 87 | var err error 88 | var certificate tls.Certificate 89 | 90 | // check if there is a certificate in the working directory 91 | execPath := os.Args[0] 92 | execDir := filepath.Dir(execPath) 93 | 94 | certPath := execDir + "/cert.crt" 95 | keyPath := execDir + "/cert.key" 96 | 97 | portEEBUS := eebusPort 98 | if len(os.Args) > 2 { 99 | if tempPort, err := strconv.Atoi(os.Args[2]); err == nil { 100 | portEEBUS = tempPort 101 | } 102 | } 103 | log.Println("EEBUS Service running at port", portEEBUS) 104 | 105 | if len(os.Args) > 3 { 106 | certPath = os.Args[3] 107 | } 108 | if len(os.Args) > 4 { 109 | keyPath = os.Args[4] 110 | } 111 | certificate, err = tls.LoadX509KeyPair(certPath, keyPath) 112 | if err != nil { 113 | certificate, err = c.createCertificate(certPath, keyPath) 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | } else { 118 | log.Println("Using certificate file", certPath, "and key file", keyPath) 119 | } 120 | 121 | serial := "123456789" 122 | if len(os.Args) > 5 { 123 | serial = os.Args[5] 124 | } 125 | eebusConfiguration, err := api.NewConfiguration( 126 | "EnbilityNet", "EnbilityNet", "Devices-App", 127 | serial, model.DeviceTypeTypeEnergyManagementSystem, 128 | []model.EntityTypeType{model.EntityTypeTypeCEM}, 129 | portEEBUS, certificate, time.Second*4) 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | 134 | c.eebusService = service.NewService(eebusConfiguration, c) 135 | c.eebusService.SetLogging(c) 136 | 137 | if err = c.eebusService.Setup(); err != nil { 138 | log.Fatal(err) 139 | return 140 | } 141 | 142 | c.eebusService.Start() 143 | } 144 | 145 | func (c *Cem) AddConnection(conn *Connection) { 146 | c.mux.Lock() 147 | defer c.mux.Unlock() 148 | 149 | c.connections[conn] = true 150 | c.sendAllowRemote(conn) 151 | } 152 | 153 | func (c *Cem) RemoveConnection(conn *Connection) { 154 | c.mux.Lock() 155 | defer c.mux.Unlock() 156 | 157 | delete(c.connections, conn) 158 | close(conn.sendChannel) 159 | } 160 | 161 | func (c *Cem) handleMessage(conn *Connection, message []byte) { 162 | var msg Message 163 | if err := json.Unmarshal(message, &msg); err != nil { 164 | return 165 | } 166 | 167 | // all are requests 168 | switch msg.Name { 169 | case MessageNameServicesList: 170 | c.sendServicesList(conn) 171 | case MessageNamePair: 172 | c.eebusService.RegisterRemoteSKI(msg.Ski) 173 | case MessageNameUnpair: 174 | c.eebusService.UnregisterRemoteSKI(msg.Ski) 175 | case MessageNameAbort: 176 | c.eebusService.CancelPairingWithSKI(msg.Ski) 177 | case MessageNameAllowRemote: 178 | c.allowRemoteConnections = msg.Enable 179 | c.broadcastAllowRemote() 180 | } 181 | } 182 | 183 | func (c *Cem) sendAllowRemote(conn *Connection) { 184 | msg := Message{ 185 | Name: MessageNameAllowRemote, 186 | Enable: c.allowRemoteConnections, 187 | } 188 | 189 | conn.sendMessage(msg) 190 | } 191 | 192 | func (c *Cem) broadcastAllowRemote() { 193 | c.mux.Lock() 194 | defer c.mux.Unlock() 195 | 196 | for conn := range c.connections { 197 | c.sendAllowRemote(conn) 198 | } 199 | } 200 | 201 | func (c *Cem) sendServicesList(conn *Connection) { 202 | c.updateServicesList() 203 | 204 | msg := Message{ 205 | Name: MessageNameServicesList, 206 | Services: c.servicesList, 207 | } 208 | 209 | conn.sendMessage(msg) 210 | } 211 | 212 | func (c *Cem) broadcastServicesList() { 213 | c.mux.Lock() 214 | defer c.mux.Unlock() 215 | 216 | for conn := range c.connections { 217 | c.sendServicesList(conn) 218 | } 219 | } 220 | 221 | // combine the mDNS entries with the service itself and all paired services 222 | func (c *Cem) updateServicesList() { 223 | servicesList := make([]*ServiceItem, 0) 224 | 225 | // add the local service first 226 | localService := &ServiceItem{ 227 | Ski: c.eebusService.LocalService().SKI(), 228 | Brand: "Enbility.net", 229 | Model: "Devices App", 230 | Itself: true, 231 | } 232 | servicesList = append(servicesList, localService) 233 | 234 | // add the mDNS records 235 | for _, element := range c.currentRemoteServices { 236 | service := c.eebusService.RemoteServiceForSKI(element.Ski) 237 | detail := service.ConnectionStateDetail() 238 | 239 | stateError := "" 240 | if detail.Error() != nil { 241 | stateError = detail.Error().Error() 242 | } 243 | 244 | newService := &ServiceItem{ 245 | Ski: element.Ski, 246 | Trusted: service.Trusted(), 247 | State: detail.State(), 248 | StateError: stateError, 249 | Brand: element.Brand, 250 | Model: element.Model, 251 | } 252 | 253 | if service.Trusted() { 254 | if data, ok := c.discoveryData[element.Ski]; ok { 255 | newService.Discovery = data 256 | } 257 | 258 | if data, ok := c.usecaseData[element.Ski]; ok { 259 | newService.UseCase = data 260 | } 261 | } 262 | 263 | servicesList = append(servicesList, newService) 264 | } 265 | 266 | // get all paired services and add them if they are not listed 267 | // some services stop publishing the mDNS entry when they are connected with a paired service 268 | // (as they do not support multiple connected services) 269 | // TODO 270 | 271 | // sort the entries by brand, model 272 | sort.Slice(servicesList, func(i, j int) bool { 273 | item1 := servicesList[i] 274 | item2 := servicesList[j] 275 | a := strings.ToLower(item1.Brand + item1.Model + item1.Ski) 276 | b := strings.ToLower(item2.Brand + item2.Model + item2.Ski) 277 | return a < b 278 | }) 279 | 280 | c.servicesMux.Lock() 281 | c.servicesList = servicesList 282 | c.servicesMux.Unlock() 283 | } 284 | 285 | // EEBUSServiceHandler 286 | 287 | func (c *Cem) RemoteSKIConnected(service api.ServiceInterface, ski string) {} 288 | 289 | func (c *Cem) RemoteSKIDisconnected(service api.ServiceInterface, ski string) { 290 | c.updateServicesList() 291 | 292 | c.broadcastServicesList() 293 | } 294 | 295 | func (c *Cem) VisibleRemoteServicesUpdated(service api.ServiceInterface, entries []shipapi.RemoteService) { 296 | c.currentRemoteServices = entries 297 | 298 | c.updateServicesList() 299 | 300 | c.broadcastServicesList() 301 | } 302 | 303 | func (c *Cem) ServiceShipIDUpdate(ski string, shipdID string) {} 304 | 305 | func (c *Cem) ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail) { 306 | // if accepted from both ends, we need to persist this 307 | if detail.State() == shipapi.ConnectionStateTrusted { 308 | c.eebusService.RegisterRemoteSKI(ski) 309 | } 310 | 311 | c.updateServicesList() 312 | 313 | c.broadcastServicesList() 314 | } 315 | 316 | // providing trust is only possible when there is a web interface connected 317 | func (c *Cem) AllowWaitingForTrust(ski string) bool { 318 | return c.allowRemoteConnections 319 | } 320 | 321 | // Logging interface 322 | 323 | func (c *Cem) Trace(args ...interface{}) { 324 | c.print("TRACE", args...) 325 | } 326 | 327 | func (c *Cem) Tracef(format string, args ...interface{}) { 328 | c.printFormat("TRACE", format, args...) 329 | } 330 | 331 | func (c *Cem) Debug(args ...interface{}) { 332 | c.print("DEBUG", args...) 333 | } 334 | 335 | func (c *Cem) Debugf(format string, args ...interface{}) { 336 | c.printFormat("DEBUG", format, args...) 337 | } 338 | 339 | func (c *Cem) Info(args ...interface{}) { 340 | c.print("INFO ", args...) 341 | } 342 | 343 | func (c *Cem) Infof(format string, args ...interface{}) { 344 | c.printFormat("INFO ", format, args...) 345 | } 346 | 347 | func (c *Cem) Error(args ...interface{}) { 348 | c.print("ERROR", args...) 349 | } 350 | 351 | func (c *Cem) Errorf(format string, args ...interface{}) { 352 | c.printFormat("ERROR", format, args...) 353 | } 354 | 355 | func (c *Cem) currentTimestamp() string { 356 | return time.Now().Format("2006-01-02 15:04:05") 357 | } 358 | 359 | func (c *Cem) print(msgType string, args ...interface{}) { 360 | value := fmt.Sprintln(args...) 361 | c.filterSpineLogs(value) 362 | fmt.Printf("%s %s %s", c.currentTimestamp(), msgType, value) 363 | } 364 | 365 | func (c *Cem) printFormat(msgType, format string, args ...interface{}) { 366 | value := fmt.Sprintf(format, args...) 367 | c.filterSpineLogs(value) 368 | fmt.Println(c.currentTimestamp(), msgType, value) 369 | } 370 | 371 | // filter out UseCase and DetailedDiscovery 372 | func (c *Cem) filterSpineLogs(msg string) { 373 | parts := strings.Split(msg, " ") 374 | if len(parts) != 3 { 375 | return 376 | } 377 | 378 | if parts[0] != "Recv:" { 379 | return 380 | } 381 | 382 | ski := parts[1] 383 | var msgService shipapi.RemoteService 384 | 385 | c.servicesMux.Lock() 386 | for _, service := range c.currentRemoteServices { 387 | if service.Ski == ski { 388 | msgService = service 389 | break 390 | } 391 | } 392 | c.servicesMux.Unlock() 393 | 394 | if len(msgService.Ski) == 0 { 395 | return 396 | } 397 | 398 | // discovery data 399 | if strings.Contains(msg, "{\"payload\":[{\"cmd\":[[{\"nodeManagementDetailedDiscoveryData\":") && 400 | !strings.Contains(msg, "{\"nodeManagementDetailedDiscoveryData\":[]") { 401 | c.discoveryData[ski] = parts[2] 402 | c.broadcastServicesList() 403 | return 404 | } 405 | 406 | // usecase data 407 | if strings.Contains(msg, "{\"payload\":[{\"cmd\":[[{\"nodeManagementUseCaseData\":") && 408 | !strings.Contains(msg, "{\"nodeManagementUseCaseData\":[]") { 409 | c.usecaseData[ski] = parts[2] 410 | c.broadcastServicesList() 411 | return 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /frontend/src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 4 | 258 | 259 | 555 | --------------------------------------------------------------------------------