Commit 99f72fbd authored by Erik Hedenström's avatar Erik Hedenström
Browse files

Added Page Monitor and MyDlink

parent 90f0dc87
.env
.token
.config.json
mydlink/src/mydlink
nipca-motion/src/nipca-motion
page-monitor/src/page-monitor
......@@ -12,13 +12,31 @@ stages:
.build: &build
stage: build
only:
- develop
- master
- /^\d+[.]\d+[.]\d+$/
script:
- /usr/bin/builder.sh -t $CI_PROJECT_DIR/${CI_JOB_NAME%%_*} --${CI_JOB_NAME#*_} -i ${CI_JOB_NAME%%_*}-addon-{arch} -d $REGISTRY
- |
if [ "$CI_COMMIT_REF_NAME" == "develop" ]; then
/usr/bin/builder.sh -t $CI_PROJECT_DIR/${CI_JOB_NAME%%_*} --${CI_JOB_NAME#*_} --test --no-latest -i ${CI_JOB_NAME%%_*}-{arch} -d local
else
/usr/bin/builder.sh -t $CI_PROJECT_DIR/${CI_JOB_NAME%%_*} --${CI_JOB_NAME#*_} -i ${CI_JOB_NAME%%_*}-addon-{arch} -d $REGISTRY
fi
"mydlink_aarch64": *build
"mydlink_amd64": *build
"mydlink_armv7": *build
"mydlink_armhf": *build
"mydlink_i386": *build
"nipca-motion_aarch64": *build
"nipca-motion_amd64": *build
"nipca-motion_armv7": *build
"nipca-motion_armhf": *build
"nipca-motion_i386": *build
"page-monitor_aarch64": *build
"page-monitor_amd64": *build
"page-monitor_armv7": *build
"page-monitor_armhf": *build
"page-monitor_i386": *build
{
"gopls": {
"experimentalWorkspaceModule": true,
}
}
# NIPCA Motion
Motion and sound sensors for NIPCA-compatible cameras.
# MyDlink
Support for MyDlink cloud connected cameras
# Page Monitor
Monitor URLs for any changes
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Run Home Assistant Build Tool",
"type": "shell",
"command": "docker run --rm -ti --privileged -v `pwd`:/data -v /var/run/docker.sock:/var/run/docker.sock:ro homeassistant/amd64-builder -t /data --amd64 --test -i mydlink-addon-{arch} -d local",
"group": "test",
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
}
]
}
## [0.1.0 2021-4-26]
### Fixes
- TBD
# Home Assistant Add-on: MyDlink
## Installation
tbd
## Configuration
tbd
ARG BUILD_FROM=amd64
FROM ${BUILD_FROM}/golang:1.16.3-alpine3.13 as golang
WORKDIR /go/src
COPY src .
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /go/bin/addon
FROM amd64/alpine:3.13 as upx
COPY --from=golang /go/bin/addon /usr/bin/addon
RUN apk add --no-cache upx
RUN upx -qq /usr/bin/addon
RUN upx -t /usr/bin/addon
FROM ${BUILD_FROM}/alpine:3.13
COPY --from=upx /usr/bin/addon /usr/bin/addon
ENTRYPOINT ["addon"]
# Home Assistant Add-on: MyDlink
## About
tbd
### Features
- tbd
- tbd
{
"build_from": {
"aarch64": "arm64v8",
"amd64": "amd64",
"armhf": "arm32v6",
"armv7": "arm32v7",
"i386": "i386"
}
}
{
"name": "MyDlink",
"version": "0.1",
"slug": "mydlink",
"description": "Support for MyDlink cloud connected cameras",
"url": "https://gitlab.hedenstroem.com/home-assistant/addons/-/tree/master/mydlink",
"init": false,
"hassio_api": true,
"arch": ["aarch64", "amd64", "armhf", "armv7", "i386"],
"options": {
"log_level": "info",
"username": "",
"password": "",
"login_host": "se.mydlink.com"
},
"schema": {
"log_level": "list(trace|debug|info|warn|error|fatal|panic)",
"username": "str",
"password": "str",
"login_host": "str"
},
"image": "gcr.io/hedenstroem-docker/mydlink-addon-{arch}"
}
module gitlab.hedenstroem.com/hassio/addons/mydlink
go 1.16
require (
github.com/davecgh/go-spew v1.1.1
github.com/gorilla/websocket v1.4.2
github.com/joho/godotenv v1.3.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/viper v1.7.1
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
)
This diff is collapsed.
package main
import (
"encoding/base64"
"encoding/json"
"strconv"
"time"
)
type InlineJson map[string]interface{}
func (i *InlineJson) UnmarshalJSON(data []byte) error {
s, err := strconv.Unquote(string(data))
if err != nil {
return err
}
var v map[string]interface{}
err = json.Unmarshal([]byte(s), &v)
if err != nil {
return err
}
*i = InlineJson(v)
return nil
}
type UTC time.Time
func (u *UTC) UnmarshalJSON(data []byte) error {
n, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
var t time.Time
if n > time.Now().Unix()*100 {
t = time.Unix(n/1000, 0)
} else {
t = time.Unix(n, 0)
}
*u = UTC(t)
return nil
}
func (u UTC) MarshalJSON() ([]byte, error) {
t := time.Time(u)
return t.MarshalJSON()
}
type LoginTime time.Time
func (l *LoginTime) UnmarshalJSON(data []byte) error {
const layout = "\"2006-01-02-15:04:05\""
t, err := time.Parse(layout, string(data))
if err != nil {
return err
}
*l = LoginTime(t)
return nil
}
func (l LoginTime) MarshalJSON() ([]byte, error) {
t := time.Time(l)
return t.MarshalJSON()
}
type Base64 []byte
func (b *Base64) UnmarshalJSON(src []byte) error {
src = src[1 : len(src)-1]
dst := make([]byte, base64.StdEncoding.DecodedLen(len(src)))
n, err := base64.StdEncoding.Decode(dst, src)
*b = Base64(dst[:n])
return err
}
func (b *Base64) MarshalJSON() ([]byte, error) {
src := []byte(*b)
dst := make([]byte, base64.StdEncoding.EncodedLen(len(src)))
base64.StdEncoding.Encode(dst, src)
return dst, nil
}
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/websocket"
"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
var log *logrus.Logger
func logJSON(level logrus.Level, label string, input interface{}) {
json, err := json.MarshalIndent(input, "| ", " ")
if err == nil {
log.Logf(level, "%s\n| %s", label, string(json))
}
}
func loadOptions() {
godotenv.Load()
viper.AutomaticEnv()
viper.BindEnv("token_file", "MYDLINK_TOKEN_FILE")
viper.BindEnv("username", "MYDLINK_USERNAME")
viper.BindEnv("password", "MYDLINK_PASSWORD")
viper.BindEnv("login_host", "MYDLINK_LOGIN_HOST")
viper.SetDefault("log_level", "info")
viper.SetDefault("token_file", ".token")
viper.SetDefault("login_host", "se.mydlink.com")
viper.AddConfigPath(viper.GetString("config_path"))
viper.SetConfigName(viper.GetString("config_name"))
viper.SetConfigType("json")
if err := viper.ReadInConfig(); err != nil {
log.Warn(err)
}
if viper.GetString("log_level") == "trace" {
viper.Debug()
}
}
func saveState(m *MyDlink) error {
b, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(viper.GetString("token_file"), b, 0644)
if err == nil {
log.Tracef("Saved State: %+v", *m)
}
return err
}
func restoreState() (*MyDlink, error) {
m := &MyDlink{}
b, err := ioutil.ReadFile(viper.GetString("token_file"))
if err != nil {
return m, err
}
err = json.Unmarshal(b, m)
log.Tracef("Restored State: %+v", *m)
return m, err
}
func main() {
log = logrus.New()
loadOptions()
level, err := logrus.ParseLevel(viper.GetString("log_level"))
if err == nil {
log.SetLevel(level)
}
log.Infof("Log level: %s", level)
m, err := restoreState()
if err != nil || m.IsExpired() {
err = m.FormLogin(viper.GetString("username"), viper.GetString("password"))
if err != nil {
log.Fatal(err)
}
err = saveState(m)
if err != nil {
log.Warn(err)
}
}
userInfo, err := m.GetUserInfo()
if err != nil {
log.Fatal(err)
}
logJSON(logrus.DebugLevel, "User Info", userInfo)
devices, err := m.GetDeviceList()
if err != nil {
log.Fatal(err)
}
for _, device := range devices {
logJSON(logrus.DebugLevel, "Device", device)
deviceInfo, err := m.GetDeviceInfo(device)
if err != nil {
log.Warn(err)
}
logJSON(logrus.DebugLevel, "Device Info", deviceInfo)
snapshot, err := m.GetSnapshot(device)
if err != nil {
log.Warn(err)
}
// logJSON(logrus.DebugLevel, "Snapshot", snapshot)
if len(snapshot.Photo) > 0 {
filename := fmt.Sprintf("snapshot_%s.png", device.MyDlinkID)
ioutil.WriteFile(filename, snapshot.Photo, 0644)
}
stream, err := m.GetStream(device)
if err != nil {
log.Warn(err)
}
logJSON(logrus.DebugLevel, "Stream", stream)
recordings, err := m.GetRecordings(device)
if err != nil {
log.Warn(err)
}
logJSON(logrus.DebugLevel, "Recordings", recordings)
}
}
func Readsocket(stream Stream) {
spew.Dump(stream)
u, err := url.Parse(stream.RTSPUrl)
if err != nil {
log.Warn(err)
}
u.Scheme = "wss"
log.Debugf("Connecting to %s", u.String())
d := websocket.DefaultDialer
d.EnableCompression = true
c, _, err := d.Dial(u.String(), nil)
if err != nil {
log.Fatal("dial:", err)
}
defer c.Close()
}
package main
import "time"
type ContactInfo struct {
Country string `json:"country"`
City string `json:"city"`
Location string `json:"location"`
}
type UserInfo struct {
Email string `json:"email"`
Firstname string `json:"first_name"`
Lastname string `json:"last_name"`
Language string `json:"language"`
EmailVerified bool `json:"email_verified"`
AccountExpired bool `json:"account_expired"`
CreatedVia string `json:"created_via"`
Nickname string `json:"nickname"`
ReceiveNews bool `json:"receive_news"`
Country string `json:"country"`
ContactInfo ContactInfo `json:"contact_info"`
UserSetting InlineJson `json:"user_setting"`
}
type Device struct {
MyDlinkID string `json:"mydlink_id"`
MACAddress string `json:"mac"`
Model string `json:"device_model"`
Name string `json:"device_name"`
HardwareVersion string `json:"hw_ver"`
Online bool `json:"online"`
Generation int `json:"gen"`
Features []interface{} `json:"hw_features"`
}
type DeviceInfo struct {
MyDlinkID string `json:"mydlink_id"`
DeviceModel string `json:"device_model"`
DeviceName string `json:"device_name"`
ActivationDate UTC `json:"activate_date"`
Online bool `json:"online"`
FirmwareUpToDate bool `json:"fw_uptodate"`
FirmwareForce bool `json:"fw_force"`
FirmwareManual bool `json:"fw_manual"`
FirmwareVersion string `json:"fw_ver"`
FirmwareLatest string `json:"fw_latest"`
AgentVersion string `json:"agent_ver"`
AgentLatest string `json:"agent_latest"`
UTCOffset int `json:"utc_offset"`
MetaInfo InlineJson `json:"meta_info"`
LoginTime LoginTime `json:"login_time"`
Units []DeviceUnit `json:"units"`
PinCode string `json:"pin_code"`
LastAccessed UTC `json:"lat"`
OlsonTZ string `json:"olson_tz"`
AutoTZ bool `json:"auto_tz"`
Storage string `json:"storage"`
ThirdParty bool `json:"controlled_by_3rd_party"`
}
type DeviceUnit struct {
UID int `json:"uid"`
Model string `json:"model"`
SubID string `json:"sub_id"`
Setting []int `json:"setting"`
Status []int `json:"status"`
Version string `json:"version"`
}
type Snapshot struct {
Photo Base64 `json:"photo"`
Size int `json:"size"`
}
type Stream struct {
RTSPUrl string `json:"rtsp_url"`
Expires time.Time `json:"expirationTime"`
}
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/url"
"regexp"
"strings"
"time"
"github.com/spf13/viper"
"golang.org/x/net/publicsuffix"
)
type MyDlink struct {
API string `json:"api_site"`
Token string `json:"access_token"`
Expires int64 `json:"expires_at"`
}
func (m *MyDlink) IsExpired() bool {
return m.Expires < time.Now().Unix()
}
func (m *MyDlink) httpGet(url string, output interface{}) error {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
log.Tracef("GET Response Body: %s", string(resBody))
return json.Unmarshal(resBody, output)
}
func (m *MyDlink) httpPost(url string, input interface{}, output interface{}) error {
client := &http.Client{}
reqBody, err := json.Marshal(input)
if err != nil {
return err
}
log.Tracef(url)
log.Tracef(string(reqBody))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
log.Tracef("POST Response Body: %s", string(resBody))
return json.Unmarshal(resBody, output)
}
func (m *MyDlink) FormLogin(username string, password string) error {
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return err
}
client := &http.Client{Jar: jar}
form := url.Values{
"email": {username},
"password": {password},
}
form.Encode()
url := fmt.Sprintf("https://%s/login", viper.GetString("login_host"))
req, err := http.NewRequest("POST", url, strings.NewReader(form.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
re := regexp.MustCompile(`baseUrl\s?=\s?['"](.*)['"]`)
match := re.FindSubmatch(resBody)
if match != nil {