Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ This is a simple reverse proxy which adds authentication token to requests to do

Also provides a simple web interface to view camera snapshots and open doors

The home page lists your own intercoms and, when the operator exposes
`/rest/v1/places/{placeId}/screen-sections`, cameras of neighboring entrances.
Neighboring cameras are viewable only with an active Pro subscription; otherwise
their cards say so. Cameras are matched by camera/group IDs rather than list
order, and the "Open door" button is shown only for your own access controls
(taken from `/rest/v1/places/{placeId}/accesscontrols` with `allowOpen`). If the
extra endpoints fail or return 404, the basic camera list is still rendered.
Every card comes with its own Home Assistant snippet; merge them under a single
`camera:` / `rest_command:` section.

## Run in Docker
Find available docker images here: https://github.com/moleus/domru/pkgs/container/domru
Please, don't use `latest` tag, because new update can break your setup
Expand Down Expand Up @@ -97,6 +107,8 @@ All other requests are forwarded to Domru API. A few of them:
| Endpoint | Method | Description |
|-----------------------------------------------------------------------------|--------|--------------------|
| `/rest/v1/forpost/cameras` | GET | Get cameras list |
| `/rest/v1/places/{placeId}/screen-sections` | GET | Additional camera sections and subscription access |
| `/rest/v1/places/{placeId}/accesscontrols/{accessControlId}/snapshots` | GET | Camera snapshot, including authorized neighboring entrances |
| `/rest/v1/places/{placeId}/accesscontrols/{accessControlId}/actions` | POST | Open door |
| `/rest/v1/subscribers/profiles/finances` | GET | Get finances |
| `/rest/v1/subscribers/profiles` | GET | Get profile info |
Expand Down
7 changes: 1 addition & 6 deletions cmd/controllers/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

"github.com/moleus/domru/pkg/auth"
"github.com/moleus/domru/pkg/domru"
"github.com/moleus/domru/pkg/domru/constants"
"github.com/moleus/domru/pkg/domru/models"
"github.com/moleus/domru/pkg/homeassistant"
)
Expand Down Expand Up @@ -57,11 +56,7 @@ func (h *Handler) renderTemplate(w http.ResponseWriter, templateName string, dat
}

func getTemplateFunctions() template.FuncMap {
return template.FuncMap{
"getSnapshotUrl": constants.GetSnapshotUrl,
"getOpenDoorUrl": constants.GetOpenDoorUrl,
"getCameraStreamUrl": constants.GetCameraStreamUrl,
}
return template.FuncMap{}
}
func (h *Handler) determineBaseURL(r *http.Request) string {
var scheme string
Expand Down
42 changes: 39 additions & 3 deletions cmd/controllers/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package controllers

import (
errors2 "errors"
"fmt"
"net/http"
"strings"

"github.com/moleus/domru/cmd/models"
"github.com/moleus/domru/pkg/authorizedhttp"
"github.com/moleus/domru/pkg/domru/helpers"
domrumodels "github.com/moleus/domru/pkg/domru/models"
)

func (h *Handler) HomeHandler(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -52,10 +55,43 @@ func (h *Handler) prepareHomePageData(r *http.Request) (models.HomePageData, err
}
}

errorsMessage := strings.Join(errors, "\n")

data.BaseURL = h.determineBaseURL(r)
data.LoginError = errorsMessage
sections := make(map[int]domrumodels.ScreenSectionsResponse)
controlsByPlace := make(map[int][]domrumodels.AccessControl)
for i, item := range data.Places.Data {
placeID := item.Place.ID
controls, exists := controlsByPlace[placeID]
if !exists {
result, err := h.domruAPI.RequestAccessControls(placeID)
controls = result.Data
if err != nil {
// The legacy embedded list can incorrectly advertise door access
// for paid neighboring cameras. Keep video metadata only on failure.
controls = append([]domrumodels.AccessControl(nil), item.Place.AccessControls...)
for j := range controls {
controls[j].AllowOpen = false
}
errors = append(errors, fmt.Sprintf("Не удалось проверить доступ к домофонам адреса %d: %v", placeID, err))
}
controlsByPlace[placeID] = controls
}
data.Places.Data[i].Place.AccessControls = controls
if _, exists := sections[placeID]; exists {
continue
}
result, err := h.domruAPI.RequestScreenSections(placeID)
sections[placeID] = result
if err != nil {
// Older operators may not expose this optional endpoint.
var upstreamErr *helpers.UpstreamError
if errors2.As(err, &upstreamErr) && upstreamErr.StatusCode == http.StatusNotFound {
continue
}
errors = append(errors, fmt.Sprintf("Не удалось загрузить дополнительные камеры адреса %d: %v", placeID, err))
}
}
data.CameraCards = buildCameraCards(data.BaseURL, data.Places, data.Cameras, sections)
data.LoginError = strings.Join(errors, "\n")

return data, nil
}
138 changes: 138 additions & 0 deletions cmd/controllers/home_cameras.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package controllers

import (
"fmt"
"sort"
"strconv"

"github.com/moleus/domru/cmd/models"
"github.com/moleus/domru/pkg/domru/constants"
domrumodels "github.com/moleus/domru/pkg/domru/models"
)

func cameraID(value interface{}) int {
id, err := strconv.Atoi(fmt.Sprint(value))
if err != nil || id <= 0 {
return 0
}
return id
}

func accessControlCameraID(ac domrumodels.AccessControl, cameras []domrumodels.Camera) int {
if id := cameraID(ac.ExternalCameraId); id != 0 {
return id
}
for _, camera := range cameras {
for _, group := range camera.ParentGroups {
if strconv.Itoa(group.ID) == ac.ForpostGroupId {
return camera.ID
}
}
}
return 0
}

func setCameraMedia(card *models.CameraCard, baseURL, snapshotURL string, allowVideo, allowSnapshot bool) {
if allowVideo && card.ID > 0 {
card.StreamURL = constants.GetCameraStreamUrl(baseURL, card.ID)
}
if allowSnapshot {
card.SnapshotURL = snapshotURL
}
switch {
case card.StreamURL != "":
card.Status = "Доступна"
case card.SnapshotURL != "":
card.Status = "Доступен снимок; видеопоток недоступен"
default:
card.Status = "Просмотр недоступен"
}
}

func buildCameraCards(baseURL string, places domrumodels.PlacesResponse, cameras domrumodels.CamerasResponse, sections map[int]domrumodels.ScreenSectionsResponse) []models.CameraCard {
var cards []models.CameraCard
seenCameras := make(map[int]bool)
seenControls := make(map[[2]int]bool)
sectionControls := make(map[[2]int]bool)
for placeID, response := range sections {
for _, section := range response.Sections {
if section.Type == "ACCESS_CONTROL_CAMERA" {
for _, camera := range section.Entities {
sectionControls[[2]int{placeID, camera.AccessControlID}] = true
}
}
}
}

for _, item := range places.Data {
place := item.Place
for _, ac := range place.AccessControls {
key := [2]int{place.ID, ac.ID}
if seenControls[key] || sectionControls[key] {
continue
}
seenControls[key] = true
id := accessControlCameraID(ac, cameras.Data)
card := models.CameraCard{
ID: id, Name: ac.Name, Section: "Мои домофоны",
ConfigName: fmt.Sprintf("domofon_%d_%d", place.ID, ac.ID),
}
setCameraMedia(&card, baseURL, constants.GetSnapshotUrl(baseURL, place.ID, ac.ID),
ac.AllowVideo, ac.AllowSlideshow || ac.PreviewAvailable)
if ac.AllowOpen {
card.OpenDoorURL = constants.GetOpenDoorUrl(baseURL, place.ID, ac.ID)
}
cards = append(cards, card)
seenCameras[id] = true
}
}

for _, item := range places.Data {
placeID := item.Place.ID
placeSections := append([]domrumodels.ScreenSection(nil), sections[placeID].Sections...)
sort.SliceStable(placeSections, func(i, j int) bool { return placeSections[i].Order < placeSections[j].Order })
for _, section := range placeSections {
if section.Type != "ACCESS_CONTROL_CAMERA" {
continue
}
for _, camera := range section.Entities {
key := [2]int{placeID, camera.AccessControlID}
if camera.AccessControlID <= 0 || seenControls[key] {
continue
}
seenControls[key] = true
id := cameraID(camera.ExternalCameraID)
if id > 0 && seenCameras[id] {
continue
}
card := models.CameraCard{
ID: id, Name: camera.Name, Section: "Соседний подъезд",
ConfigName: fmt.Sprintf("domofon_%d_%d", placeID, camera.AccessControlID),
Status: "Требуется подписка Pro",
}
if camera.ServiceActivated {
snapshotURL := fmt.Sprintf("%s/rest/v1/places/%d/accesscontrols/%d/snapshots?width=320&height=180", baseURL, placeID, camera.AccessControlID)
setCameraMedia(&card, baseURL, snapshotURL, camera.AllowVideo, camera.AllowSlideshow || camera.PreviewAvailable)
}
cards = append(cards, card)
seenCameras[id] = true
}
}
}

// Keep standalone account cameras that are not associated with a door.
for _, camera := range cameras.Data {
if camera.ID <= 0 || seenCameras[camera.ID] {
continue
}
card := models.CameraCard{
ID: camera.ID, Name: camera.Name, Section: "Другие камеры",
ConfigName: fmt.Sprintf("domru_camera_%d", camera.ID),
}
snapshotURL := fmt.Sprintf("%s/rest/v1/forpost/cameras/%d/snapshots?width=320&height=180", baseURL, camera.ID)
setCameraMedia(&card, baseURL, snapshotURL, camera.IsActive == 1, camera.IsActive == 1)
cards = append(cards, card)
seenCameras[camera.ID] = true
}
return cards
}
Loading