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

Camera port #263

Merged
merged 3 commits into from
Jan 22, 2024
Merged
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
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
Loading