Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for multi-server setup, enable auth token in url #53

Closed
wants to merge 1 commit into from
Closed
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
23 changes: 20 additions & 3 deletions zoneminder/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from urllib.parse import urlencode

from requests import post
import json

from .exceptions import ControlTypeError, MonitorControlTypeError

Expand Down Expand Up @@ -88,6 +89,7 @@ class Monitor:

def __init__(self, client, raw_result):
"""Create a new Monitor."""
#_LOGGER.error("INIT for monitor %s.", json.dumps(raw_result, sort_keys=False, indent=4))
self._client = client
self._raw_result = raw_result
raw_monitor = raw_result["Monitor"]
Expand Down Expand Up @@ -164,7 +166,7 @@ def is_recording(self) -> Optional[bool]:
status = status_response.get("status")
# ZoneMinder API returns an empty string to indicate that this monitor
# cannot record right now
if status == "":
if status == "" or status == "Ok":
return False
return int(status) == STATE_ALARM

Expand All @@ -174,6 +176,7 @@ def is_available(self) -> bool:
status_response = self._client.get_state(
"api/monitors/daemonStatus/id:{}/daemon:zmc.json".format(self._monitor_id)
)
#_LOGGER.error("AVAILABILITY for monitor %s .", json.dumps(status_response, sort_keys=False, indent=4))

if not status_response:
_LOGGER.warning("Could not get availability for monitor %s.", self._monitor_id)
Expand All @@ -182,8 +185,12 @@ def is_available(self) -> bool:
# Monitor_Status was only added in ZM 1.32.3
monitor_status = self._raw_result.get("Monitor_Status", None)
capture_fps = monitor_status and monitor_status["CaptureFPS"]
monitor_state = monitor_status and monitor_status["Status"]
#_LOGGER.error("STATUS for monitor %s .", json.dumps(monitor_status, sort_keys=False, indent=4))
#_LOGGER.error("FPS for monitor %s .", json.dumps(capture_fps, sort_keys=False, indent=4))

return status_response.get("status", False) and capture_fps != "0.00"
#return status_response.get("status", False) and capture_fps != "0.00"
return monitor_state == "Connected" and capture_fps != "0.00"

def get_events(self, time_period, include_archived=False) -> Optional[int]:
"""Get the number of events that have occurred on this Monitor.
Expand Down Expand Up @@ -222,7 +229,17 @@ def _build_image_url(self, monitor, mode) -> str:
"monitor": monitor["Id"],
}
)
url = "{zms_url}?{query}".format(zms_url=self._client.get_zms_url(), query=query)

_LOGGER.debug("_build_image_url for monitor %s.", json.dumps(monitor, sort_keys=False, indent=4))

if int(monitor["ServerId"]) > 0:
server = self._client._servers_by_id[int(monitor["ServerId"])]
server_hostname = "{protocol}://{hostname}{pathtozms}".format(hostname=server.hostname, pathtozms=server.pathtozms, protocol=server.protocol)
else:
server_hostname = self._client.get_zms_url()
_LOGGER.debug("_build_image_url server_hostname %s", server_hostname)

url = "{zms_url}?{query}".format(zms_url=server_hostname, query=query)
_LOGGER.debug("Monitor %s %s URL (without auth): %s", monitor["Id"], mode, url)
return self._client.get_url_with_auth(url)

Expand Down
66 changes: 66 additions & 0 deletions zoneminder/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Classes that allow interacting with specific ZoneMinder servers."""

import logging
from urllib.parse import urlencode

from requests import post
import json

from .exceptions import ControlTypeError

_LOGGER = logging.getLogger(__name__)

class Server:
"""Represents a Server from ZoneMinder."""

def __init__(self, client, raw_result):
"""Create a new Server."""
_LOGGER.debug("INIT for server %s.", json.dumps(raw_result, sort_keys=False, indent=4))
self._client = client
self._raw_result = raw_result
raw_server = raw_result["Server"]
self._server_id = int(raw_server["Id"])
self._name = raw_server["Name"]
self._protocol = raw_server["Protocol"]
self._hostname = raw_server["Hostname"]
self._pathtozms = raw_server["PathToZMS"]
self._pathtoindex = raw_server["PathToIndex"]
self._pathtoapi = raw_server["PathToApi"]
self._status = raw_server["Status"]
self._fmt = "{}(id={}, name={}, status={})"

def __repr__(self) -> str:
"""Representation of a Server."""
return self._fmt.format(self.__class__.__name__, self.id, self.name)

def __str__(self) -> str:
"""Representation of a Server."""
return self.__repr__()

@property
def id(self) -> int:
"""Get the ZoneMinder id number of this Server."""
# pylint: disable=invalid-name
return self._server_id

@property
def name(self) -> str:
"""Get the name of this Server."""
return self._name

@property
def protocol(self) -> str:
"""Get the protocol of this Server."""
return self._protocol

@property
def hostname(self) -> str:
"""Get the hostname of this Server."""
return self._hostname

@property
def pathtozms(self) -> str:
"""Get the ZMS path of this Server."""
return self._pathtozms


50 changes: 45 additions & 5 deletions zoneminder/zm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from urllib.parse import quote

import requests
import json

from zoneminder.monitor import Monitor
from zoneminder.server import Server
from zoneminder.run_state import RunState
from zoneminder.exceptions import ControlTypeError, MonitorControlTypeError

Expand All @@ -21,6 +23,7 @@ class ZoneMinder:
DEFAULT_TIMEOUT = 10
LOGIN_RETRIES = 2
MONITOR_URL = "api/monitors.json"
SERVER_URL = "api/servers.json"

def __init__(
self,
Expand All @@ -39,6 +42,8 @@ def __init__(
self._verify_ssl = verify_ssl
self._cookies = None
self._auth_token = None
self._servers_by_id = {}
self._servers = None

def login(self):
"""Login to the ZoneMinder API."""
Expand All @@ -58,6 +63,7 @@ def login(self):
if req.ok:
try:
self._auth_token = req.json()["access_token"]
_LOGGER.debug("Using access_token for login to ZoneMinder")
return True
except KeyError:
# Try legacy auth below
Expand Down Expand Up @@ -143,6 +149,10 @@ def _zm_request(self, method, api_url, data=None, timeout=DEFAULT_TIMEOUT) -> di

def get_monitors(self) -> List[Monitor]:
"""Get a list of Monitors from the ZoneMinder API."""

if not self._servers:
self._servers = self.get_servers()

raw_monitors = self._zm_request("get", ZoneMinder.MONITOR_URL)
if not raw_monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
Expand All @@ -155,6 +165,30 @@ def get_monitors(self) -> List[Monitor]:

return monitors

def get_servers(self) -> List[Server]:
"""Get a list of Servers from the ZoneMinder API."""
raw_servers = self._zm_request("get", ZoneMinder.SERVER_URL)
_LOGGER.debug("INIT for servers %s.", json.dumps(raw_servers, sort_keys=False, indent=4))
if not raw_servers:
_LOGGER.warning("Could not fetch servers from ZoneMinder")
return []

servers = []
for raw_result in raw_servers["servers"]:
_LOGGER.debug("Initializing server %s - %s", raw_result["Server"]["Id"], raw_result["Server"]["Hostname"])
servers.append(Server(self, raw_result))

if not servers:
_LOGGER.warning("Could not fetch servers from ZoneMinder host: %s")
return

for server in servers:
_LOGGER.debug("Initializing server %s", server.id)
self._servers_by_id[int(server.id)] = server
_LOGGER.debug("_servers_by_id %s.", self._servers_by_id.__str__)

return servers

def get_run_states(self) -> List[RunState]:
"""Get a list of RunStates from the ZoneMinder API."""
raw_states = self.get_state("api/states.json")
Expand Down Expand Up @@ -196,14 +230,19 @@ def get_zms_url(self) -> str:

def get_url_with_auth(self, url) -> str:
"""Add the auth credentials to a url (if needed)."""
if not self._username:

if not self._username and not self._auth_token:
return url

url += "&user={:s}".format(quote(self._username))
if self._auth_token:
url += "&token={:s}".format(quote(self._auth_token))
else:
url += "&user={:s}".format(quote(self._username))
if not self._password:
return url
url += "&pass={:s}".format(quote(self._password))

if not self._password:
return url
return url + "&pass={:s}".format(quote(self._password))
return url

@property
def is_available(self) -> bool:
Expand All @@ -223,6 +262,7 @@ def verify_ssl(self) -> bool:
@staticmethod
def _build_zms_url(server_host, zms_path) -> str:
"""Build the ZMS url to the current ZMS instance."""
_LOGGER.debug("Building ZMS URL: %s : %s", server_host, zms_path)
return urljoin(server_host, zms_path)

@staticmethod
Expand Down