Skip to content

Commit

Permalink
Camera port (#263)
Browse files Browse the repository at this point in the history
* Adding configurable path for camera

* add testing

* update doc
  • Loading branch information
marcolivierarsenault authored Jan 22, 2024
1 parent a3963d5 commit c191705
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 13 deletions.
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// Please keep this file in sync with settings in home-assistant/.devcontainer/devcontainer.json
// Added --no-cov to work around TypeError: message must be set
// https://github.com/microsoft/vscode-python/issues/14067
"python.testing.pytestArgs": ["--no-cov"],
"python.testing.pytestArgs": ["."],
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
"python.testing.pytestEnabled": false
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false
}
2 changes: 1 addition & 1 deletion configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ tts:
logger:
default: warning
logs:
custom_components.moonraker: debug
custom_components.moonraker: warning
30 changes: 26 additions & 4 deletions custom_components/moonraker/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import CONF_URL, DOMAIN, METHODS, PRINTSTATES
from .const import (
CONF_URL,
CONF_OPTION_CAMERA_STREAM,
CONF_OPTION_CAMERA_SNAPSHOT,
DOMAIN,
METHODS,
PRINTSTATES,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -40,12 +47,27 @@ async def async_setup_entry(
camera_cnt = 0

try:
cameras = await coordinator.async_fetch_data(METHODS.SERVER_WEBCAMS_LIST)
for camera_id, camera in enumerate(cameras["webcams"]):
if (
config_entry.options.get(CONF_OPTION_CAMERA_STREAM) is not None
and config_entry.options.get(CONF_OPTION_CAMERA_STREAM) != ""
):
hardcoded_camera["stream_url"] = config_entry.options.get(
CONF_OPTION_CAMERA_STREAM
)
hardcoded_camera["snapshot_url"] = config_entry.options.get(
CONF_OPTION_CAMERA_SNAPSHOT
)
async_add_entities(
[MoonrakerCamera(config_entry, coordinator, camera, camera_id)]
[MoonrakerCamera(config_entry, coordinator, hardcoded_camera, 100)]
)
camera_cnt += 1
else:
cameras = await coordinator.async_fetch_data(METHODS.SERVER_WEBCAMS_LIST)
for camera_id, camera in enumerate(cameras["webcams"]):
async_add_entities(
[MoonrakerCamera(config_entry, coordinator, camera, camera_id)]
)
camera_cnt += 1
except Exception:
_LOGGER.info("Could not add any cameras from the API list")

Expand Down
60 changes: 58 additions & 2 deletions custom_components/moonraker/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
"""Adds config flow for Moonraker."""
import logging

from typing import Any

import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import network, slugify

from .api import MoonrakerApiClient
from .const import (CONF_API_KEY, CONF_PORT, CONF_PRINTER_NAME, CONF_TLS,
CONF_URL, DOMAIN, TIMEOUT)
from .const import (
CONF_API_KEY,
CONF_PORT,
CONF_PRINTER_NAME,
CONF_TLS,
CONF_URL,
CONF_OPTION_CAMERA_STREAM,
CONF_OPTION_CAMERA_SNAPSHOT,
DOMAIN,
TIMEOUT,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -126,3 +139,46 @@ async def _test_connection(self, host, port, api_key, tls):
return True
except Exception:
return False

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_OPTION_CAMERA_STREAM,
default=self.config_entry.options.get(
CONF_OPTION_CAMERA_STREAM, ""
),
): str,
vol.Optional(
CONF_OPTION_CAMERA_SNAPSHOT,
default=self.config_entry.options.get(
CONF_OPTION_CAMERA_SNAPSHOT, ""
),
): str,
}
),
)
2 changes: 2 additions & 0 deletions custom_components/moonraker/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
CONF_PORT = "port"
CONF_TLS = "tls"
CONF_PRINTER_NAME = "printer_name"
CONF_OPTION_CAMERA_STREAM = "camera_stream_url"
CONF_OPTION_CAMERA_SNAPSHOT = "camera_snapshot_url"

# API dict keys
HOSTNAME = "hostname"
Expand Down
11 changes: 11 additions & 0 deletions custom_components/moonraker/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,16 @@
"printer_name_error": "Invalid printer name.",
"printer_connection_error": "Failed to connect."
}
},
"options": {
"step": {
"init": {
"data": {
"camera_stream_url": "Camera Stream URL",
"camera_snapshot_url": "Camera Snapshot URL"
},
"title": "Configuration Camera"
}
}
}
}
Binary file added docs/_static/config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion docs/connection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ Connection properties can be defined as follows:
- Device name in Home Assistant

Note: Encrypted connections must be configured in Moonraker API or by using a
reverse proxy to connect to Moonraker API.
reverse proxy to connect to Moonraker API.


Camera Manual Configuration
-------------------------------------

Camera URL can be manually defined more details in the :ref:`camera_config`
11 changes: 10 additions & 1 deletion docs/entities/camera.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ Camera

* Thumbnail of the current print. (Your slicer needs to generate the thumbnail image) |thum_image|

.. _camera_config:

Manual Configuration
-------------------------------------

It is possible to manually configure the Stream and Snapshot URL for the camera. This will bypass the automatic configuration.

|config|

.. |cam_image| image:: https://raw.githubusercontent.com/marcolivierarsenault/moonraker-home-assistant/main/assets/camera.png
.. |thum_image| image:: https://raw.githubusercontent.com/marcolivierarsenault/moonraker-home-assistant/main/assets/thumbnail.png
.. |thum_image| image:: https://raw.githubusercontent.com/marcolivierarsenault/moonraker-home-assistant/main/assets/thumbnail.png
.. |config| image:: /_static/config.png
7 changes: 7 additions & 0 deletions tests/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
CONF_PRINTER_NAME,
CONF_TLS,
CONF_URL,
CONF_OPTION_CAMERA_STREAM,
CONF_OPTION_CAMERA_SNAPSHOT,
)

# Mock config data to be used across multiple tests
Expand All @@ -16,6 +18,11 @@
CONF_PRINTER_NAME: "",
}

MOCK_OPTIONS = {
CONF_OPTION_CAMERA_STREAM: "http://1.2.3.4/stream",
CONF_OPTION_CAMERA_SNAPSHOT: "http://1.2.3.4/snapshot",
}

MOCK_CONFIG_WITH_NAME = {
CONF_URL: "1.2.3.4",
CONF_PORT: "1234",
Expand Down
19 changes: 18 additions & 1 deletion tests/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from custom_components.moonraker import async_setup_entry
from custom_components.moonraker.const import DOMAIN, PRINTSTATES

from .const import MOCK_CONFIG
from .const import MOCK_CONFIG, MOCK_OPTIONS


@pytest.fixture(name="bypass_connect_client", autouse=True)
Expand Down Expand Up @@ -286,3 +286,20 @@ async def test_thumbnail_on_subfolder(hass, get_data, aioclient_mock):

await camera.async_get_image(hass, "camera.mainsail_thumbnail")
await camera.async_get_image(hass, "camera.mainsail_thumbnail")


async def test_option_config_camera_services(hass, caplog):
"""Test camera services."""

config_entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS, entry_id="test"
)
config_entry.add_to_hass(hass)
assert await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()

entity_registry = er.async_get(hass)
entry = entity_registry.async_get("camera.mainsail_webcam")

assert entry is not None
assert "Connecting to camera: http://1.2.3.4/stream" in caplog.text
31 changes: 30 additions & 1 deletion tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@

import pytest
from homeassistant import config_entries, data_entry_flow
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.moonraker import async_setup_entry
from custom_components.moonraker.const import (
CONF_API_KEY,
CONF_PORT,
CONF_PRINTER_NAME,
CONF_TLS,
CONF_URL,
CONF_OPTION_CAMERA_SNAPSHOT,
CONF_OPTION_CAMERA_STREAM,
DOMAIN,
)

from .const import MOCK_CONFIG
from .const import MOCK_CONFIG, MOCK_OPTIONS


@pytest.fixture(name="bypass_connect_client")
Expand Down Expand Up @@ -395,3 +399,28 @@ async def test_bad_connection_config_flow(hass):
)

assert result["errors"] == {CONF_URL: "printer_connection_error"}


@pytest.mark.usefixtures("bypass_connect_client")
async def test_option_config_camera_services(hass):
"""Test a config flow with camera services."""
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
config_entry.add_to_hass(hass)
assert await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()

result = await hass.config_entries.options.async_init("test")
await hass.async_block_till_done()

assert result["type"] == data_entry_flow.FlowResultType.FORM

result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_OPTION_CAMERA_STREAM: MOCK_OPTIONS[CONF_OPTION_CAMERA_STREAM],
CONF_OPTION_CAMERA_SNAPSHOT: MOCK_OPTIONS[CONF_OPTION_CAMERA_SNAPSHOT],
},
)
await hass.async_block_till_done()

assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY

0 comments on commit c191705

Please sign in to comment.