Compare commits
No commits in common. "main" and "v0.3.10" have entirely different histories.
15 changed files with 52 additions and 225 deletions
|
@ -7,7 +7,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- run: apk add --no-cache nodejs git
|
- run: apk add --no-cache nodejs git
|
||||||
- name: login to container registry
|
- name: login to container registry
|
||||||
run: echo "${{ secrets.PACKAGE_PUBLISH_TOKEN }}" | docker login --username ${{ secrets.PACKAGE_PUBLISH_USER }} --password-stdin git.janky.solutions
|
run: echo "${{ secrets.PACKAGE_PUBLISH_TOKEN }}" | docker login --username finn --password-stdin git.janky.solutions
|
||||||
- name: gather metadata for container image tags
|
- name: gather metadata for container image tags
|
||||||
uses: https://github.com/docker/metadata-action@v5
|
uses: https://github.com/docker/metadata-action@v5
|
||||||
id: meta
|
id: meta
|
||||||
|
@ -20,9 +20,7 @@ jobs:
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
|
push: true
|
||||||
build-args: |
|
|
||||||
VERSION_STRING=${{ env.GITHUB_REF_NAME }}
|
|
||||||
- name: update hassio-addons
|
- name: update hassio-addons
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -18,8 +18,7 @@ FROM alpine:latest AS build
|
||||||
RUN apk add --no-cache go
|
RUN apk add --no-cache go
|
||||||
ADD . /go/lockserver
|
ADD . /go/lockserver
|
||||||
WORKDIR /go/lockserver
|
WORKDIR /go/lockserver
|
||||||
ARG VERSION_STRING
|
RUN CGO_ENABLED=0 go build ./cmd/lockserver
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-X git.janky.solutions/finn/lockserver/config.Version=${VERSION_STRING}" ./cmd/lockserver
|
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=build /go/lockserver/lockserver /lockserver
|
COPY --from=build /go/lockserver/lockserver /lockserver
|
||||||
|
|
18
README.md
18
README.md
|
@ -1,18 +0,0 @@
|
||||||
# lockserver
|
|
||||||
|
|
||||||
_better Z-Wave Lock management for Home Assistant_
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
This is a work in progress. I have some ideas of where I want it to go, but I'm mostly experimenting with my own needs.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
To add to Home Assistant, add my hassio-addons repo by clicking the button below, then search for and install the "LockServer" addon.
|
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgit.janky.solutions%2Ffinn%2Fhassio-addons)
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
When you open the addon's web UI, it will show a list of Z-Wave locks. Clicking a lock shows all codes slots for that lock. Clicking edit on each slot allows changing the code, changing the name, enabling or disabling the slot, and seeing a log of recent uses of that code.
|
|
|
@ -1,34 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime/debug"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
BuildInfo *debug.BuildInfo
|
|
||||||
Version string
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var ok bool
|
|
||||||
BuildInfo, ok = debug.ReadBuildInfo()
|
|
||||||
if !ok {
|
|
||||||
logrus.Error("failed to read build info")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if Version == "" {
|
|
||||||
for _, setting := range BuildInfo.Settings {
|
|
||||||
if setting.Key == "vcs.revision" {
|
|
||||||
Version = setting.Value
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if Version == "" {
|
|
||||||
Version = "development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -49,11 +49,10 @@ func Migrate() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NullString(s string) sql.NullString {
|
func NullString(s string) sql.NullString {
|
||||||
return sql.NullString{Valid: s != "", String: s}
|
return sql.NullString{
|
||||||
}
|
Valid: s != "",
|
||||||
|
String: s,
|
||||||
func NullInt64(i int64) sql.NullInt64 {
|
}
|
||||||
return sql.NullInt64{Valid: true, Int64: i}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type loggingDBTX struct {
|
type loggingDBTX struct {
|
||||||
|
|
|
@ -25,44 +25,6 @@ func (q *Queries) AddLogEntry(ctx context.Context, arg AddLogEntryParams) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLastLogForSlot = `-- name: GetLastLogForSlot :many
|
|
||||||
SELECT lock, timestamp, state, code, issued_code FROM lock_log WHERE lock = ? AND code = ? ORDER BY timestamp DESC LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetLastLogForSlotParams struct {
|
|
||||||
Lock int64
|
|
||||||
Code sql.NullInt64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetLastLogForSlot(ctx context.Context, arg GetLastLogForSlotParams) ([]LockLog, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, getLastLogForSlot, arg.Lock, arg.Code)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []LockLog
|
|
||||||
for rows.Next() {
|
|
||||||
var i LockLog
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.Lock,
|
|
||||||
&i.Timestamp,
|
|
||||||
&i.State,
|
|
||||||
&i.Code,
|
|
||||||
&i.IssuedCode,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLogForLock = `-- name: GetLogForLock :many
|
const getLogForLock = `-- name: GetLogForLock :many
|
||||||
SELECT lock, timestamp, state, code, issued_code FROM lock_log WHERE lock = ? ORDER BY timestamp DESC
|
SELECT lock, timestamp, state, code, issued_code FROM lock_log WHERE lock = ? ORDER BY timestamp DESC
|
||||||
`
|
`
|
||||||
|
@ -95,41 +57,3 @@ func (q *Queries) GetLogForLock(ctx context.Context, lock int64) ([]LockLog, err
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLogForSlot = `-- name: GetLogForSlot :many
|
|
||||||
SELECT lock, timestamp, state, code, issued_code FROM lock_log WHERE lock = ? AND code = ? ORDER BY timestamp DESC LIMIT 100
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetLogForSlotParams struct {
|
|
||||||
Lock int64
|
|
||||||
Code sql.NullInt64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetLogForSlot(ctx context.Context, arg GetLogForSlotParams) ([]LockLog, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, getLogForSlot, arg.Lock, arg.Code)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []LockLog
|
|
||||||
for rows.Next() {
|
|
||||||
var i LockLog
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.Lock,
|
|
||||||
&i.Timestamp,
|
|
||||||
&i.State,
|
|
||||||
&i.Code,
|
|
||||||
&i.IssuedCode,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,9 +3,3 @@ INSERT INTO lock_log (lock, state, code) VALUES (?, ?, ?);
|
||||||
|
|
||||||
-- name: GetLogForLock :many
|
-- name: GetLogForLock :many
|
||||||
SELECT * FROM lock_log WHERE lock = ? ORDER BY timestamp DESC;
|
SELECT * FROM lock_log WHERE lock = ? ORDER BY timestamp DESC;
|
||||||
|
|
||||||
-- name: GetLogForSlot :many
|
|
||||||
SELECT * FROM lock_log WHERE lock = ? AND code = ? ORDER BY timestamp DESC LIMIT 100;
|
|
||||||
|
|
||||||
-- name: GetLastLogForSlot :many
|
|
||||||
SELECT * FROM lock_log WHERE lock = ? AND code = ? ORDER BY timestamp DESC LIMIT 1;
|
|
||||||
|
|
|
@ -4,9 +4,6 @@ import (
|
||||||
"embed"
|
"embed"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.janky.solutions/finn/lockserver/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -19,8 +16,7 @@ var (
|
||||||
Templates *template.Template
|
Templates *template.Template
|
||||||
|
|
||||||
funcs = template.FuncMap{
|
funcs = template.FuncMap{
|
||||||
"version": func() string { return config.Version },
|
"version": func() string { return "better-zwave-locks v0.x.x aaaaaaaa" },
|
||||||
"time_since": func(t time.Time) string { return time.Since(t).Round(time.Second).String() },
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<title>Better Z-Wave Locks for Home Assistant</title>
|
<title>Better Z-Wave Locks for Home Assistant</title>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||||
<link rel="stylesheet" href="{{ .BaseURL }}/static-{{ version }}/main.css" />
|
<link rel="stylesheet" href="{{ .BaseURL }}/static/main.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,11 +1,28 @@
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
<h1>Better Z-Wave Locks</h1>
|
<h1>Better Z-Wave Locks</h1>
|
||||||
<br /><br />
|
<!-- <header>Active Codes</header>
|
||||||
<span class="table">
|
[ <a href="{{ .BaseURL }}/add-code">add</a> ]
|
||||||
{{ range .Locks }}
|
<table border="1">
|
||||||
<a class="table-row" href="{{ $.BaseURL }}/locks/{{ .ID }}">
|
<tr>
|
||||||
<p class="table-cell">{{ if eq .Name "" }}Lock #{{ .ZwaveDeviceID }}{{ else }}{{ .Name }}{{ end }}</p>
|
<th>Name</th>
|
||||||
</a>
|
<th>Code</th>
|
||||||
|
<th>Last used</th>
|
||||||
|
<th>Expires</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
{{ range .ActiveCodes }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ .Name.Value }}</td>
|
||||||
|
<td>{{ .Code }}</td>
|
||||||
|
<td>0.005ms ago on <a href="{{ $.BaseURL }}/locks/1">Back Door</a></td>
|
||||||
|
<td>{{ .End.Value }}</td>
|
||||||
|
<td><a href="#">details</a> | <a href="#">delete</a></td>
|
||||||
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</span>
|
</table> -->
|
||||||
|
<br /><br />
|
||||||
|
<header>locks</header>
|
||||||
|
<ul>
|
||||||
|
{{ range .Locks }}<li><a href="{{ $.BaseURL }}/locks/{{ .ID }}">{{ .Name }} ({{ .ZwaveDeviceID }})</a></li>{{ end }}
|
||||||
|
</ul>
|
||||||
{{ template "footer.html" }}
|
{{ template "footer.html" }}
|
||||||
|
|
|
@ -2,27 +2,10 @@
|
||||||
<header>{{ if eq .Data.lock.Name "" }}Lock #{{ .Data.lock.ID }}{{ else }}{{ .Data.lock.Name }}{{ end }} Slot #{{ .Data.code.Slot }}</header>
|
<header>{{ if eq .Data.lock.Name "" }}Lock #{{ .Data.lock.ID }}{{ else }}{{ .Data.lock.Name }}{{ end }} Slot #{{ .Data.code.Slot }}</header>
|
||||||
<br />
|
<br />
|
||||||
<form method="post">
|
<form method="post">
|
||||||
Code: <input type="text" name="code" value="{{ .Data.code.Code }}" id="code" /> <a href="#" onclick="generateCode()">🔄</a><br />
|
Code: <input type="text" name="code" value="{{ .Data.code.Code }}" /><br />
|
||||||
Name: <input type="text" name="name" value="{{ .Data.code.Name }}" /><br />
|
Name: <input type="text" name="name" value="{{ .Data.code.Name }}" /><br />
|
||||||
Enabled: <input type="checkbox" name="enabled" {{ if .Data.code.Enabled }}checked{{ end }} /><br />
|
Enabled: <input type="checkbox" name="enabled" {{ if .Data.code.Enabled }}checked{{ end }} /><br />
|
||||||
<br />
|
<br />
|
||||||
<input type="submit" value="save" />
|
<input type="submit" value="save" />
|
||||||
</form>
|
</form>
|
||||||
<br /><br />
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{{ range $_, $entry := .Data.log }}
|
|
||||||
<li>{{ $entry.State }} (<i>{{ $entry.Timestamp | time_since }} ago</i>)</li>
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
function generateCode() {
|
|
||||||
let code = "";
|
|
||||||
while(code.length < 4) {
|
|
||||||
code += Math.round(Math.random()*10);
|
|
||||||
}
|
|
||||||
document.querySelector('#code').value = code;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{{ template "footer.html" }}
|
{{ template "footer.html" }}
|
||||||
|
|
|
@ -2,19 +2,22 @@
|
||||||
<header>{{ if eq .Data.lock.Name "" }}Lock #{{ .Data.lock.ID }}{{ else }}{{ .Data.lock.Name }}{{ end }}</header>
|
<header>{{ if eq .Data.lock.Name "" }}Lock #{{ .Data.lock.ID }}{{ else }}{{ .Data.lock.Name }}{{ end }}</header>
|
||||||
[ <a href="{{ $.BaseURL }}/locks/{{ .Data.lock.ID }}/edit">rename</a> ]<br />
|
[ <a href="{{ $.BaseURL }}/locks/{{ .Data.lock.ID }}/edit">rename</a> ]<br />
|
||||||
<br />
|
<br />
|
||||||
<span class="table">
|
<table border="1">
|
||||||
<span class="table-row">
|
<tr>
|
||||||
<span class="table-cell">Name</span>
|
<td>Slot</td>
|
||||||
<span class="table-cell">Code</span>
|
<td>Name</td>
|
||||||
</span>
|
<td>Code</td>
|
||||||
|
<td>Enabled?</td>
|
||||||
|
<td>Actions</td>
|
||||||
|
</tr>
|
||||||
{{ range $_, $code := .Data.codes }}
|
{{ range $_, $code := .Data.codes }}
|
||||||
<a href="{{ $.BaseURL }}/locks/{{ $.Data.lock.ID }}/codes/{{ $code.Slot }}" class="table-row code-{{ if $code.Enabled }}enabled{{ else }}disabled{{ end }}">
|
<tr class="code-{{ if $code.Enabled }}enabled{{ else }}disabled{{ end }}">
|
||||||
<span class="table-cell">{{ $code.Name }}</span>
|
<td>{{ $code.Slot }}</td>
|
||||||
<span class="table-cell">{{ $code.Code }}</span>
|
<td>{{ $code.Name }}</td>
|
||||||
</a>
|
<td>{{ $code.Code }}</td>
|
||||||
|
<td>{{ if $code.Enabled }}enabled{{ else }}disabled{{ end }}</td>
|
||||||
|
<td>[ <a href="{{ $.BaseURL }}/locks/{{ $.Data.lock.ID }}/codes/{{ $code.Slot }}">edit</a> ]</td>
|
||||||
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</span>
|
</table>
|
||||||
|
|
||||||
<br /><br />
|
|
||||||
|
|
||||||
{{ template "footer.html" }}
|
{{ template "footer.html" }}
|
||||||
|
|
|
@ -16,8 +16,6 @@ a {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
row-gap: 1em;
|
row-gap: 1em;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: auto;
|
|
||||||
max-width: 1000px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#main {
|
#main {
|
||||||
|
@ -30,24 +28,5 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-enabled {
|
.code-enabled {
|
||||||
background-color: #050;
|
background-color: #0a0;
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row {
|
|
||||||
border-bottom: solid #999 1px;
|
|
||||||
/* background-color: #333; */
|
|
||||||
height: 3em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row:first {
|
|
||||||
border-top: solid #999 1px;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package httpserver
|
package httpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -10,7 +8,6 @@ import (
|
||||||
"git.janky.solutions/finn/lockserver/db"
|
"git.janky.solutions/finn/lockserver/db"
|
||||||
"git.janky.solutions/finn/lockserver/zwavejs"
|
"git.janky.solutions/finn/lockserver/zwavejs"
|
||||||
echo "github.com/labstack/echo/v4"
|
echo "github.com/labstack/echo/v4"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func lockHandler(c echo.Context) error {
|
func lockHandler(c echo.Context) error {
|
||||||
|
@ -112,19 +109,9 @@ func lockCodeEditHandler(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Request().Method == http.MethodGet {
|
if c.Request().Method == http.MethodGet {
|
||||||
logrus.WithField("lock", lockID).WithField("code", code.ID).Debug("querying logs")
|
|
||||||
log, err := queries.GetLogForSlot(ctx, db.GetLogForSlotParams{
|
|
||||||
Lock: lockID,
|
|
||||||
Code: db.NullInt64(code.ID),
|
|
||||||
})
|
|
||||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Render(http.StatusFound, "lock-code-edit.html", map[string]interface{}{
|
return c.Render(http.StatusFound, "lock-code-edit.html", map[string]interface{}{
|
||||||
"lock": lock,
|
"lock": lock,
|
||||||
"code": code,
|
"code": code,
|
||||||
"log": log,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ func ListenAndServe(zwaveClient *zwavejs.Client) {
|
||||||
server.Use(accessLogMiddleware)
|
server.Use(accessLogMiddleware)
|
||||||
server.RouteNotFound("/*", tmpl("404.html"))
|
server.RouteNotFound("/*", tmpl("404.html"))
|
||||||
|
|
||||||
server.StaticFS("/static-"+config.Version, frontend.Static)
|
server.StaticFS("/static", frontend.Static)
|
||||||
server.GET("/", indexHandler)
|
server.GET("/", indexHandler)
|
||||||
server.GET("/locks/:lock", lockHandler)
|
server.GET("/locks/:lock", lockHandler)
|
||||||
server.GET("/locks/:lock/edit", lockEditHandler)
|
server.GET("/locks/:lock/edit", lockEditHandler)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue