Skip to content

Commit 59788b2

Browse files
committed
feat(MOTION): expose settings and improve security
1 parent 0891400 commit 59788b2

29 files changed

+775
-227
lines changed

README.md

+6-46
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Please use at your own risk.
3434
4. Wiring between the switches, temperature monitors and the Raspberry Pi's GPIO connectors.
3535
- Find out more about the Pi's GPIO [here](https://projects.raspberrypi.org/en/projects/physical-computing).
3636
- Female [jump wires](https://en.wikipedia.org/wiki/Jump_wire) make installing the connections pretty painless. I spliced them to the ends of modular cables (i.e. phone cables) for longer runs.
37-
- Edit the [config.json](./config.json) file to customize your pin outs and integrations.
37+
- Edit the [config.json](config.json) file to customize your pin outs and integrations.
3838
5. A USB camera or webcam that's compatible with [motion](https://motion-project.github.io/).
3939
- [Many](https://www.lavrsen.dk/foswiki/bin/view/Motion/WorkingDevices) webcams are compatible, and easy to find.
4040

@@ -87,52 +87,12 @@ PI Portal ships with a binary for [filebeat](https://www.elastic.co/beats/filebe
8787

8888
### Creating a configuration file
8989

90-
Create a configuration json file that contains the following:
90+
To get started rolling your own configuration file, here's some important resources:
9191

92-
```json
93-
{
94-
"ARCHIVAL": {
95-
"AWS": {
96-
"AWS_ACCESS_KEY_ID": "... AWS key with write access to buckets ...",
97-
"AWS_SECRET_ACCESS_KEY": "... AWS secret key with write access buckets ...",
98-
"AWS_S3_BUCKETS": {
99-
"LOGS": "... s3 logs bucket name ...",
100-
"VIDEOS": "... s3 video bucket name ..."
101-
}
102-
}
103-
},
104-
"CHAT": {
105-
"SLACK": {
106-
"SLACK_APP_SIGNING_SECRET": "... secret value from slack to validate bot messages ...",
107-
"SLACK_APP_TOKEN": "... token from slack to allow app to use websockets ...",
108-
"SLACK_BOT_TOKEN": "... token from slack...",
109-
"SLACK_CHANNEL": "... proper name of slack channel ...",
110-
"SLACK_CHANNEL_ID": ".. slack's ID for the channel ..."
111-
}
112-
},
113-
"LOGS": {
114-
"LOGZ_IO": {
115-
"LOGZ_IO_TOKEN": "... logz io's logger token ..."
116-
}
117-
},
118-
"SWITCHES": {
119-
"CONTACT_SWITCHES": [
120-
{
121-
"NAME": "... name and pin-out of a GPIO switch...",
122-
"GPIO": 12
123-
}
124-
]
125-
},
126-
"TEMPERATURE_SENSORS": {
127-
"DHT11": [
128-
{
129-
"NAME": "... name and pin-out of a GPIO with a DHT11 connected ...",
130-
"GPIO": 4
131-
}
132-
]
133-
}
134-
}
135-
```
92+
1. First it's handy to look at the [sample config](config.json) included in the repository.
93+
2. For more detail on what the options all do, there's a JSON schema [documented here](https://pi-portal.readthedocs.io/en/stable/project/5.configuration.html) that can help you make the most of your config.
94+
3. The actual JSON schema is [here](pi_portal/schema/config_schema.json) and is used to do programmatic validation of your configuration.
95+
4. The motion configuration can be a bit overwhelming. The existing values in the [sample config](config.json) should get you started. You can also check out the [motion](pi_portal/installation/templates/motion/motion.conf) and [camera](pi_portal/installation/templates/motion/camera.conf) configuration files for a bit more information.
13696

13797
### Installing The PI Portal Software
13898

config.json

+31
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,37 @@
99
}
1010
}
1111
},
12+
"MOTION": {
13+
"AUTHENTICATION": {
14+
"USERNAME": "username",
15+
"PASSWORD": "password"
16+
},
17+
"CAMERAS": [
18+
{
19+
"DEVICE": "/dev/video0",
20+
"IMAGE": {
21+
"FRAME_RATE": 5,
22+
"WIDTH": 320,
23+
"HEIGHT": 240,
24+
"AUTO_BRIGHTNESS": "off",
25+
"BRIGHTNESS": 0,
26+
"CONTRAST": 0,
27+
"SATURATION": 0,
28+
"HUE": 0
29+
}
30+
}
31+
],
32+
"DETECTION": {
33+
"THRESHOLD": 1500,
34+
"EVENT_GAP": 60
35+
},
36+
"MOVIES": {
37+
"LOCATE_MOTION_MODE": "on"
38+
},
39+
"SNAPSHOTS": {
40+
"QUALITY": 100
41+
}
42+
},
1243
"CHAT": {
1344
"SLACK": {
1445
"SLACK_APP_SIGNING_SECRET": "... secret value from slack to validate bot messages ...",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. _pi_portal/installation/templates/motion/camera.conf:
2+
3+
pi_portal/installation/templates/motion/camera.conf
4+
===================================================
5+
6+
.. literalinclude:: ../../../../pi_portal/installation/templates/motion/camera.conf
7+
:language: conf
8+
:linenos:

documentation/source/project/content/config.json.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.. _./config.json:
1+
.. _config.json:
22

33
Example Configuration File
44
==========================
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. _pi_portal/schema/config_schema.json:
2+
3+
pi_portal/schema/config_schema.json
4+
===================================
5+
6+
.. literalinclude:: ../../../../pi_portal/schema/config_schema.json
7+
:language: json
8+
:linenos:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. _pi_portal/installation/templates/motion/motion.conf:
2+
3+
pi_portal/installation/templates/motion/motion.conf
4+
===================================================
5+
6+
.. literalinclude:: ../../../../pi_portal/installation/templates/motion/motion.conf
7+
:language: conf
8+
:linenos:

pi_portal/installation/installer.py

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pi_portal.modules.configuration.logging import installer
77
from .steps import (
88
StepConfigureLogzIo,
9+
StepConfigureMotion,
910
StepEnsureRoot,
1011
StepInitializeDataPaths,
1112
StepInitializeEtc,
@@ -43,6 +44,7 @@ def _configure_steps(self) -> List[base_step.StepBase]:
4344
StepInitializeDataPaths(self.log),
4445
StepInitializeEtc(self.log),
4546
StepInitializeLogging(self.log),
47+
StepConfigureMotion(self.log),
4648
StepRenderConfiguration(self.log),
4749
StepInstallConfigFile(self.log, self.config_file_path),
4850
StepConfigureLogzIo(self.log),

pi_portal/installation/steps/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Step classes for the installation process."""
22

33
from .step_configure_logz_io import StepConfigureLogzIo
4+
from .step_configure_motion import StepConfigureMotion
45
from .step_ensure_root import StepEnsureRoot
56
from .step_initialize_data_paths import StepInitializeDataPaths
67
from .step_initialize_etc import StepInitializeEtc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""StepConfigureMotion class."""
2+
3+
from typing import List
4+
5+
from pi_portal.installation.templates import config_file, motion_templates
6+
from pi_portal.modules.configuration import state
7+
from .bases import render_templates_step
8+
9+
10+
class StepConfigureMotion(render_templates_step.RenderTemplateStepBase):
11+
"""Render the motion configuration."""
12+
13+
templates: List[config_file.ConfileFileTemplate] = motion_templates
14+
15+
def invoke(self) -> None:
16+
"""Render the templated configuration."""
17+
18+
self.log.info("Rendering motion configuration ...")
19+
self.generate_camera_templates()
20+
self.render()
21+
self.log.info("Done rendering motion configuration.")
22+
23+
def generate_camera_templates(self) -> None:
24+
"""Generate config templates for each camera in user configuration."""
25+
26+
user_config = state.State().user_config
27+
28+
for index, camera in enumerate(user_config["MOTION"]["CAMERAS"]):
29+
self.log.info(
30+
"Creating template for '%s' ...",
31+
camera["DEVICE"],
32+
)
33+
camera_config_file = config_file.ConfileFileTemplate(
34+
source='motion/camera.conf',
35+
destination=f'/etc/motion/camera{index}.conf',
36+
permissions="600",
37+
user="root",
38+
)
39+
camera_config_file.context["CAMERA"] = camera
40+
camera_config_file.context["CAMERA"]["NAME"] = f"CAMERA-{index}"
41+
camera_config_file.context["CAMERA"]["ID"] = index
42+
self.templates.append(camera_config_file)

pi_portal/installation/steps/tests/conftest.py

+22
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
from unittest import mock
88

99
import pytest
10+
from pi_portal.modules.configuration import state
1011
from pi_portal.modules.configuration.logging import installer
1112
from .. import (
1213
step_configure_logz_io,
14+
step_configure_motion,
1315
step_ensure_root,
1416
step_initialize_data_paths,
1517
step_initialize_etc,
@@ -117,6 +119,26 @@ def step_configure_logz_io_instance(
117119
return step_configure_logz_io.StepConfigureLogzIo(installer_logger_stdout)
118120

119121

122+
@pytest.fixture
123+
def step_configure_motion_instance(
124+
installer_logger_stdout: logging.Logger,
125+
mocked_template_render: mock.Mock,
126+
mocked_state: state.State,
127+
monkeypatch: pytest.MonkeyPatch,
128+
) -> step_configure_motion.StepConfigureMotion:
129+
monkeypatch.setattr(
130+
render_templates_step.__name__ + ".RenderTemplateStepBase.render",
131+
mocked_template_render,
132+
)
133+
monkeypatch.setattr(
134+
state.State(),
135+
"user_config",
136+
mocked_state.user_config,
137+
)
138+
instance = step_configure_motion.StepConfigureMotion(installer_logger_stdout)
139+
return instance
140+
141+
120142
@pytest.fixture
121143
def step_ensure_root_instance(
122144
installer_logger_stdout: logging.Logger,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Test the StepRenderConfiguration class."""
2+
import logging
3+
import os
4+
from io import StringIO
5+
from unittest import mock
6+
7+
from pi_portal.installation.templates import config_file, motion_templates
8+
from pi_portal.modules.configuration import state
9+
from ..step_configure_motion import StepConfigureMotion
10+
11+
12+
class TestStepRenderTemplates:
13+
"""Test the StepRenderConfiguration class."""
14+
15+
def test__initialize__attrs(
16+
self,
17+
step_configure_motion_instance: StepConfigureMotion,
18+
) -> None:
19+
assert isinstance(step_configure_motion_instance.log, logging.Logger)
20+
assert step_configure_motion_instance.templates == motion_templates
21+
22+
def test__invoke__success__logging(
23+
self,
24+
step_configure_motion_instance: StepConfigureMotion,
25+
mocked_state: state.State,
26+
mocked_stream: StringIO,
27+
mocked_system: mock.Mock,
28+
) -> None:
29+
mocked_system.return_value = 0
30+
camera_config = mocked_state.user_config["MOTION"]["CAMERAS"]
31+
expected_log_messages = ""
32+
for camera in camera_config:
33+
expected_log_messages += \
34+
f"test - INFO - Creating template for '{camera['DEVICE']}' ...\n"
35+
36+
step_configure_motion_instance.invoke()
37+
38+
assert mocked_stream.getvalue() == (
39+
"test - INFO - Rendering motion configuration ...\n" +
40+
expected_log_messages +
41+
"test - INFO - Done rendering motion configuration.\n"
42+
)
43+
44+
def test__invoke__render_call(
45+
self,
46+
step_configure_motion_instance: StepConfigureMotion,
47+
mocked_template_render: mock.Mock,
48+
) -> None:
49+
step_configure_motion_instance.invoke()
50+
51+
mocked_template_render.assert_called_once_with()
52+
53+
def test__generate_camera_templates__updates_templates(
54+
self,
55+
step_configure_motion_instance: StepConfigureMotion,
56+
mocked_state: state.State,
57+
) -> None:
58+
camera_config = mocked_state.user_config["MOTION"]["CAMERAS"]
59+
motion_template_count = len(motion_templates)
60+
assert step_configure_motion_instance.templates == \
61+
motion_templates
62+
63+
step_configure_motion_instance.generate_camera_templates()
64+
65+
assert len(step_configure_motion_instance.templates) == \
66+
motion_template_count + len(camera_config)
67+
68+
def test__generate_camera_templates__creates_valid_templates(
69+
self,
70+
step_configure_motion_instance: StepConfigureMotion,
71+
mocked_state: state.State,
72+
) -> None:
73+
camera_config = mocked_state.user_config["MOTION"]["CAMERAS"]
74+
motion_template_count = len(motion_templates)
75+
76+
step_configure_motion_instance.generate_camera_templates()
77+
78+
camera_templates = step_configure_motion_instance.\
79+
templates[motion_template_count:]
80+
for index, template in enumerate(camera_templates):
81+
assert template.source == os.path.join(
82+
os.path.dirname(config_file.__file__),
83+
"motion/camera.conf",
84+
)
85+
assert template.destination == f'/etc/motion/camera{index}.conf'
86+
assert template.permissions == "600"
87+
assert template.user == "root"
88+
assert template.context["CAMERA"] == camera_config[index]
89+
assert template.context["CAMERA"]["NAME"] == f"CAMERA-{index}"
90+
assert template.context["CAMERA"]["ID"] == index

pi_portal/installation/steps/tests/test_step_render_configuration.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ..step_render_configuration import StepRenderConfiguration
88

99

10-
class TestStepRenderTemplates:
10+
class TestStepRenderConfiguration:
1111
"""Test the StepRenderConfiguration class."""
1212

1313
def test__initialize__attrs(

pi_portal/installation/templates/__init__.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
from .config_file import ConfileFileTemplate
77

88
common_templates: List[ConfileFileTemplate] = [
9-
ConfileFileTemplate(
10-
source='motion/motion.conf',
11-
destination='/etc/motion/motion.conf',
12-
),
139
ConfileFileTemplate(
1410
source='shim/portal',
1511
destination=config.PI_PORTAL_SHIM,
@@ -27,3 +23,10 @@
2723
destination='/etc/filebeat/filebeat.yml',
2824
),
2925
]
26+
27+
motion_templates: List[ConfileFileTemplate] = [
28+
ConfileFileTemplate(
29+
source='motion/motion.conf',
30+
destination='/etc/motion/motion.conf',
31+
),
32+
]

0 commit comments

Comments
 (0)