Code quality improvement on local_file (#125165)
* Code quality improvement on local_file * Fix * No translation * Review comments
This commit is contained in:
parent
051a28b55a
commit
9f469c08d1
4 changed files with 121 additions and 74 deletions
|
@ -12,13 +12,14 @@ from homeassistant.components.camera import (
|
|||
PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
|
||||
Camera,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_FILE_PATH, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_FILE_PATH, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DATA_LOCAL_FILE, DEFAULT_NAME, DOMAIN, SERVICE_UPDATE_FILE_PATH
|
||||
from .const import DEFAULT_NAME, SERVICE_UPDATE_FILE_PATH
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,57 +30,45 @@ PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend(
|
|||
}
|
||||
)
|
||||
|
||||
CAMERA_SERVICE_UPDATE_FILE_PATH = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
def check_file_path_access(file_path: str) -> bool:
|
||||
"""Check that filepath given is readable."""
|
||||
if not os.access(file_path, os.R_OK):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def setup_platform(
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Camera that works with local files."""
|
||||
if DATA_LOCAL_FILE not in hass.data:
|
||||
hass.data[DATA_LOCAL_FILE] = []
|
||||
file_path: str = config[CONF_FILE_PATH]
|
||||
|
||||
file_path = config[CONF_FILE_PATH]
|
||||
camera = LocalFile(config[CONF_NAME], file_path)
|
||||
hass.data[DATA_LOCAL_FILE].append(camera)
|
||||
|
||||
def update_file_path_service(call: ServiceCall) -> None:
|
||||
"""Update the file path."""
|
||||
file_path = call.data[CONF_FILE_PATH]
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
cameras = hass.data[DATA_LOCAL_FILE]
|
||||
|
||||
for camera in cameras:
|
||||
if camera.entity_id in entity_ids:
|
||||
camera.update_file_path(file_path)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_UPDATE_FILE_PATH,
|
||||
update_file_path_service,
|
||||
schema=CAMERA_SERVICE_UPDATE_FILE_PATH,
|
||||
{
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
},
|
||||
"update_file_path",
|
||||
)
|
||||
|
||||
add_entities([camera])
|
||||
if not await hass.async_add_executor_job(check_file_path_access, file_path):
|
||||
raise PlatformNotReady(f"File path {file_path} is not readable")
|
||||
|
||||
async_add_entities([LocalFile(config[CONF_NAME], file_path)])
|
||||
|
||||
|
||||
class LocalFile(Camera):
|
||||
"""Representation of a local file camera."""
|
||||
|
||||
def __init__(self, name, file_path):
|
||||
def __init__(self, name: str, file_path: str) -> None:
|
||||
"""Initialize Local File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._name = name
|
||||
self.check_file_path_access(file_path)
|
||||
self._attr_name = name
|
||||
self._file_path = file_path
|
||||
# Set content type of local file
|
||||
content, _ = mimetypes.guess_type(file_path)
|
||||
|
@ -96,30 +85,21 @@ class LocalFile(Camera):
|
|||
except FileNotFoundError:
|
||||
_LOGGER.warning(
|
||||
"Could not read camera %s image from file: %s",
|
||||
self._name,
|
||||
self.name,
|
||||
self._file_path,
|
||||
)
|
||||
return None
|
||||
|
||||
def check_file_path_access(self, file_path):
|
||||
"""Check that filepath given is readable."""
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.warning(
|
||||
"Could not read camera %s image from file: %s", self._name, file_path
|
||||
)
|
||||
|
||||
def update_file_path(self, file_path):
|
||||
async def update_file_path(self, file_path: str) -> None:
|
||||
"""Update the file_path."""
|
||||
self.check_file_path_access(file_path)
|
||||
if not await self.hass.async_add_executor_job(
|
||||
check_file_path_access, file_path
|
||||
):
|
||||
raise ServiceValidationError(f"Path {file_path} is not accessible")
|
||||
self._file_path = file_path
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return the camera state attributes."""
|
||||
return {"file_path": self._file_path}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
update_file_path:
|
||||
target:
|
||||
entity:
|
||||
integration: local_file
|
||||
domain: camera
|
||||
fields:
|
||||
entity_id:
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
domain: camera
|
||||
file_path:
|
||||
required: true
|
||||
example: "/config/www/images/image.jpg"
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
"name": "Updates file path",
|
||||
"description": "Use this action to change the file displayed by the camera.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Name of the entity_id of the camera to update."
|
||||
},
|
||||
"file_path": {
|
||||
"name": "File path",
|
||||
"description": "The full path to the new image file to be displayed."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"file_path_not_accessible": {
|
||||
"message": "Path {file_path} is not accessible"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ from unittest import mock
|
|||
import pytest
|
||||
|
||||
from homeassistant.components.local_file.const import DOMAIN, SERVICE_UPDATE_FILE_PATH
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_FILE_PATH
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
@ -71,9 +73,45 @@ async def test_file_not_readable(
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Could not read" in caplog.text
|
||||
assert "config_test" in caplog.text
|
||||
assert "mock.file" in caplog.text
|
||||
assert "File path mock.file is not readable;" in caplog.text
|
||||
|
||||
|
||||
async def test_file_not_readable_after_setup(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a warning is shown setup when file is not readable."""
|
||||
with (
|
||||
mock.patch("os.path.isfile", mock.Mock(return_value=True)),
|
||||
mock.patch("os.access", mock.Mock(return_value=True)),
|
||||
mock.patch(
|
||||
"homeassistant.components.local_file.camera.mimetypes.guess_type",
|
||||
mock.Mock(return_value=(None, None)),
|
||||
),
|
||||
):
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"camera",
|
||||
{
|
||||
"camera": {
|
||||
"name": "config_test",
|
||||
"platform": "local_file",
|
||||
"file_path": "mock.file",
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with mock.patch(
|
||||
"homeassistant.components.local_file.camera.open", side_effect=FileNotFoundError
|
||||
):
|
||||
resp = await client.get("/api/camera_proxy/camera.config_test")
|
||||
|
||||
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
assert "Could not read camera config_test image from file: mock.file" in caplog.text
|
||||
|
||||
|
||||
async def test_camera_content_type(
|
||||
|
@ -100,13 +138,23 @@ async def test_camera_content_type(
|
|||
"platform": "local_file",
|
||||
"file_path": "/path/to/image",
|
||||
}
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"camera",
|
||||
{"camera": [cam_config_jpg, cam_config_png, cam_config_svg, cam_config_noext]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with (
|
||||
mock.patch("os.path.isfile", mock.Mock(return_value=True)),
|
||||
mock.patch("os.access", mock.Mock(return_value=True)),
|
||||
):
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"camera",
|
||||
{
|
||||
"camera": [
|
||||
cam_config_jpg,
|
||||
cam_config_png,
|
||||
cam_config_svg,
|
||||
cam_config_noext,
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
|
@ -169,8 +217,12 @@ async def test_update_file_path(hass: HomeAssistant) -> None:
|
|||
|
||||
service_data = {"entity_id": "camera.local_file", "file_path": "new/path.jpg"}
|
||||
|
||||
await hass.services.async_call(DOMAIN, SERVICE_UPDATE_FILE_PATH, service_data)
|
||||
await hass.async_block_till_done()
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_FILE_PATH,
|
||||
service_data,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get("camera.local_file")
|
||||
assert state.attributes.get("file_path") == "new/path.jpg"
|
||||
|
@ -178,3 +230,18 @@ async def test_update_file_path(hass: HomeAssistant) -> None:
|
|||
# Check that local_file_camera_2 file_path is still as configured
|
||||
state = hass.states.get("camera.local_file_camera_2")
|
||||
assert state.attributes.get("file_path") == "mock/path_2.jpg"
|
||||
|
||||
# Assert it fails if file is not readable
|
||||
service_data = {
|
||||
ATTR_ENTITY_ID: "camera.local_file",
|
||||
CONF_FILE_PATH: "new/path2.jpg",
|
||||
}
|
||||
with pytest.raises(
|
||||
ServiceValidationError, match="Path new/path2.jpg is not accessible"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_FILE_PATH,
|
||||
service_data,
|
||||
blocking=True,
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue