initial commit

This commit is contained in:
Erki 2024-05-25 23:49:18 +03:00
commit 84b956b7f7
19 changed files with 731 additions and 0 deletions

45
.devcontainer.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "skullnet/sys_mon",
"image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
],
"portsAttributes": {
"8123": {
"label": "Home Assistant",
"onAutoForward": "notify"
}
},
"runArgs": [
"--add-host=host.docker.internal:host-gateway"
],
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": false,
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
}
}
},
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers/features/rust:1": {}
}
}

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# artifacts
__pycache__
.pytest*
*.egg-info
*/build/*
*/dist/*
# misc
.coverage
.vscode
coverage.xml
# Home Assistant configuration
config/*
!config/configuration.yaml

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "custom_components/sys_mon/sys_mon_agent"]
path = custom_components/sys_mon/sys_mon_agent
url = https://git.skullnet.me/erki/ha-sys-mon-agent.git

48
.ruff.toml Normal file
View File

@ -0,0 +1,48 @@
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
target-version = "py310"
select = [
"B007", # Loop control variable {name} not used within loop body
"B014", # Exception handler with duplicate exception
"C", # complexity
"D", # docstrings
"E", # pycodestyle
"F", # pyflakes/autoflake
"ICN001", # import concentions; {name} should be imported as {asname}
"PGH004", # Use specific rule codes when using noqa
"PLC0414", # Useless import alias. Import alias does not rename original package.
"SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass
"SIM117", # Merge with-statements that use the same scope
"SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys()
"SIM201", # Use {left} != {right} instead of not {left} == {right}
"SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a}
"SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'.
"SIM401", # Use get from dict with default instead of an if block
"T20", # flake8-print
"TRY004", # Prefer TypeError exception for invalid type
"RUF006", # Store a reference to the return value of asyncio.create_task
"UP", # pyupgrade
"W", # pycodestyle
]
ignore = [
"D202", # No blank lines allowed after function docstring
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the second line
"D404", # First word of the docstring should not be This
"D406", # Section name should end with a newline
"D407", # Section name underlining
"D411", # Missing blank line before section
"E501", # line too long
"E731", # do not assign a lambda expression, use a def
]
[flake8-pytest-style]
fixture-parentheses = false
[pyupgrade]
keep-runtime-typing = true
[mccabe]
max-complexity = 25

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Rusted Skull
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

26
config/configuration.yaml Normal file
View File

@ -0,0 +1,26 @@
# https://www.home-assistant.io/integrations/default_config/
default_config:
# https://www.home-assistant.io/integrations/logger/
logger:
default: info
logs:
custom_components.sys_mon: debug
sys_mon:
agents:
- address: "127.0.0.1:8202"
hostname: "local.skullnet.me"
services_to_monitor: [
]
storage_to_monitor: [
]
- address: "host.docker.internal:8203"
hostname: "localhost"
services_to_monitor: [
"sshd.service",
"docker.service"
]
storage_to_monitor: [
"/dev/sda"
]

View File

@ -0,0 +1,37 @@
"""Custom integration to integrate sys_mon with Home Assistant."""
from __future__ import annotations
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import ConfigType
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, LOGGER, CONF_STORAGE, CONF_SERVICES, CONF_AGENTS
from .coordinator import SysMonCoordinator
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
LOGGER.warning(f"We're actually here. Woups? {config[DOMAIN]}")
coord = SysMonCoordinator(hass)
hass.data[DOMAIN] = {}
# We go do this shit: https://github.com/home-assistant/example-custom-config/blob/master/custom_components/example_load_platform/__init__.py
for entry in config[DOMAIN][CONF_AGENTS]:
LOGGER.debug(f"Agent entry: {entry}")
await coord.add_agent(entry)
hass.helpers.discovery.load_platform("sensor", DOMAIN, {"coordinator": coord, "config": config[DOMAIN][CONF_AGENTS]}, config)
await coord.async_config_entry_first_refresh()
LOGGER.debug(f"Coordinator data: {coord.data}")
return True

View File

@ -0,0 +1,17 @@
"""Constants for Sys Mon."""
from logging import Logger, getLogger
LOGGER: Logger = getLogger(__package__)
NAME = "Sys Mon"
DOMAIN = "sys_mon"
VERSION = "0.1.0"
ATTRIBUTION = "who me"
CONF_SERVICES = "services_to_monitor"
CONF_STORAGE = "storage_to_monitor"
CONF_AGENTS = "agents"
CONF_HOSTNAME = "hostname"
STATE_IS_CONNECTED = "connected"
STATE_DATA = "data"

View File

@ -0,0 +1,137 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.const import CONF_ADDRESS
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DOMAIN, LOGGER, CONF_SERVICES, CONF_STORAGE, CONF_HOSTNAME, STATE_DATA, STATE_IS_CONNECTED
import grpc
from google.protobuf.empty_pb2 import Empty
from .sys_mon_agent import api
_EMPTY_AGENT_DATA = {
STATE_IS_CONNECTED: False,
STATE_DATA: None
}
@dataclass
class MonitoredAgent:
address: str
hostname: str
configuration_data: dict[str: Any]
configured: bool = False
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
class SysMonCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from various hosts."""
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant
) -> None:
"""Initialize."""
super().__init__(
hass=hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=10),
)
self._client_data: list[MonitoredAgent] = []
async def add_agent(self, agent_conf):
agent = MonitoredAgent(agent_conf[CONF_ADDRESS], agent_conf[CONF_HOSTNAME], agent_conf, configured=False)
agent.configured = await self._configure_agent(agent)
self._client_data.append(agent)
async def _async_update_data(self):
"""Update data via library."""
try:
# this will populate ["something"]["state"] exciting.
ctx = list(self.async_contexts())
LOGGER.info(f"SysMonCoordinator is updating data. Context: {ctx}")
if len(ctx) == 0:
data = {}
for agent in self._client_data:
if agent.configured:
data[agent.hostname] = await self._poll_host(agent)
else:
data[agent.hostname] = _EMPTY_AGENT_DATA
LOGGER.debug(f"SysMonCoordinator final coordinator data: {data}.")
return data
else:
for hostname in ctx:
agent = [a for a in self._client_data if a.hostname == hostname][0]
if not agent.configured:
success = await self._configure_agent(agent)
if success:
agent.configured = True
self.data[hostname] = await self._poll_host(agent)
else:
self.data[hostname] = _EMPTY_AGENT_DATA
else:
self.data[hostname] = await self._poll_host(agent)
LOGGER.debug(f"SysMonCoordinator final coordinator data: {self.data}.")
return self.data
except Exception as exception:
raise UpdateFailed(exception) from exception
async def _poll_host(self, client: MonitoredAgent) -> dict[str, Any]:
try:
async with grpc.aio.insecure_channel(client.address) as channel:
stub = api.AgentStub(channel)
response: api.MonitoringStats = await stub.Poll(
Empty()
)
data = {
STATE_IS_CONNECTED: True,
STATE_DATA: response
}
return data
except Exception as err:
LOGGER.warning(f"SysMonCoordinator error polling host {client.hostname}: {err}.")
return _EMPTY_AGENT_DATA
async def _configure_agent(self, agent: MonitoredAgent) -> bool:
try:
async with grpc.aio.insecure_channel(agent.address) as channel:
stub = api.AgentStub(channel)
services: list[str] | None = None
if agent.configuration_data[CONF_SERVICES] and len(agent.configuration_data[CONF_SERVICES]):
services = agent.configuration_data[CONF_SERVICES]
storage: list[str] | None = None
if agent.configuration_data[CONF_STORAGE] and len(agent.configuration_data[CONF_STORAGE]):
storage = agent.configuration_data[CONF_STORAGE]
response: api.AgentConfigurationResponse = await stub.Configure(
api.AgentConfiguration(
services_to_monitor=services,
storage_to_monitor=storage
)
)
LOGGER.debug(f"SysMonCoordinator successfully configured agent {agent.hostname}.")
return True
except grpc.aio.AioRpcError as rpc_error:
LOGGER.warning(f"SysMonCoordinator failed to configure agent {agent.hostname}.")
return False

View File

@ -0,0 +1,17 @@
from homeassistant.core import callback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
UpdateFailed,
)
from .coordinator import SysMonCoordinator
from .const import DOMAIN, LOGGER
class BaseSysMonEntity(CoordinatorEntity):
def __init__(self, coordinator: SysMonCoordinator, hostname: str):
super().__init__(coordinator, context=hostname)
self.hostname = hostname
@callback
def _handle_coordinator_update(self) -> None:
LOGGER.debug(f"We have data: {self.coordinator.data}")

View File

@ -0,0 +1,15 @@
{
"domain": "sys_mon",
"name": "Sys Mon",
"codeowners": [
"@skull132"
],
"iot_class": "cloud_polling",
"version": "0.1.0",
"requirements": [
"dbus-next",
"grpcio",
"protobuf",
"pySMART"
]
}

View File

@ -0,0 +1,293 @@
from __future__ import annotations
from typing import Any, Optional
import datetime
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass
)
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .entity import BaseSysMonEntity
from .coordinator import SysMonCoordinator
from .const import CONF_HOSTNAME, CONF_STORAGE, CONF_SERVICES, STATE_IS_CONNECTED, STATE_DATA, LOGGER
from .sys_mon_agent import api
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None
) -> None:
if discovery_info is None:
return
hostname_list = [agent[CONF_HOSTNAME] for agent in discovery_info["config"]]
LOGGER.debug(f"Adding sensors. Recognized agent hostnames: {hostname_list}")
entities = []
for agent in discovery_info["config"]:
entities.append(AgentStatusSensor(discovery_info["coordinator"], agent[CONF_HOSTNAME]))
if CONF_STORAGE in agent:
for storage in agent[CONF_STORAGE]:
entities.append(StorageStatusSensor(discovery_info["coordinator"], agent[CONF_HOSTNAME], storage))
if CONF_SERVICES in agent:
for service in agent[CONF_SERVICES]:
entities.append(ServiceStateSensor(discovery_info["coordinator"], agent[CONF_HOSTNAME], service))
entities.append(ServiceStartedSensor(discovery_info["coordinator"], agent[CONF_HOSTNAME], service))
entities.append(ServiceStoppedSensor(discovery_info["coordinator"], agent[CONF_HOSTNAME], service))
entities.append(ServiceResultStatusSensor(discovery_info["coordinator"], agent[CONF_HOSTNAME], service))
add_entities(entities)
class AgentStatusSensor(BinarySensorEntity, BaseSysMonEntity):
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
def __init__(self, coordinator: SysMonCoordinator, hostname: str):
super().__init__(coordinator, hostname)
self._hostname = hostname
self._attr_is_on = None
LOGGER.debug(f"AgentStatusSensor[{self._hostname}] created.")
@property
def name(self) -> str:
return f"SysMon Agent Status: {self._hostname}"
@callback
def _handle_coordinator_update(self) -> None:
LOGGER.debug(f"AgentStatusSensor[{self._hostname}] polled state. Received: {self.coordinator.data[self._hostname]}. State: {self.coordinator.data[self._hostname][STATE_IS_CONNECTED]}")
self._attr_is_on = self.coordinator.data[self._hostname][STATE_IS_CONNECTED]
self.async_write_ha_state()
class StorageStatusSensor(SensorEntity, BaseSysMonEntity):
_attr_device_class = SensorDeviceClass.ENUM
_attr_options = [
"HEALTHY",
"FAILING",
"MISSING"
]
def __init__(self, coordinator: SysMonCoordinator, hostname: str, storage_name: str):
super().__init__(coordinator, hostname)
self._hostname = hostname
self._storage_name = storage_name
self._attr_native_value = None
LOGGER.debug(f"{self.name_log}: created.")
@property
def name_log(self) -> str:
return f"StorageStatusSensor[{self._hostname}][{self._storage_name}]"
@property
def name(self) -> str:
return f"SysMon {self._hostname} storage: {self._storage_name}"
@callback
def _handle_coordinator_update(self) -> None:
if not self.coordinator.data[self._hostname][STATE_IS_CONNECTED]:
if self._attr_native_value is not None:
self._attr_native_value = None
self.async_write_ha_state()
LOGGER.debug(f"{self.name_log}: polled state. Received: {self.coordinator.data[self._hostname]}.")
return
LOGGER.debug(f"{self.name_log}: polled state. Received: {self.coordinator.data[self._hostname]}.")
state_data: api.MonitoringStats = self.coordinator.data[self._hostname][STATE_DATA]
data: Optional[api.MonitoredStorage] = state_data.storage_by_name(self._storage_name)
if not data:
self._attr_native_value = None
elif not data.present:
self._attr_native_value = "MISSING"
elif data.smart_pass:
self._attr_native_value = "HEALTHY"
else:
self._attr_native_value = "FAILING"
self.async_write_ha_state()
class ServiceStateSensor(SensorEntity, BaseSysMonEntity):
_attr_device_class = SensorDeviceClass.ENUM
_attr_options = [
"ACTIVE",
"RELOADING",
"INACTIVE",
"FAILED",
"ACTIVATING",
"DEACTIVATING"
]
def __init__(self, coordinator: SysMonCoordinator, hostname: str, service_name: str):
super().__init__(coordinator, hostname)
self._hostname = hostname
self._service_name = service_name
self._attr_native_value = None
LOGGER.debug(f"{self.name_log}: created.")
@property
def name_log(self) -> str:
return f"ServiceStateSensor[{self._hostname}][{self._service_name}]"
@property
def name(self) -> str:
return f"SysMon {self._hostname} service: {self._service_name}"
@callback
def _handle_coordinator_update(self) -> None:
if not self.coordinator.data[self._hostname][STATE_IS_CONNECTED]:
if self._attr_native_value is not None:
self._attr_native_value = None
self.async_write_ha_state()
LOGGER.debug(f"{self.name_log}: polled state. Agent is NOT connected. Received: {self.coordinator.data[self._hostname]}.")
return
LOGGER.debug(f"{self.name_log}: polled state. Agent IS connected. Received: {self.coordinator.data[self._hostname]}.")
state_data: api.MonitoringStats = self.coordinator.data[self._hostname][STATE_DATA]
data: Optional[api.MonitoredService] = state_data.service_by_name(self._service_name)
if data is None:
self._attr_native_value = None
else:
self._attr_native_value = self._attr_options[data.state]
self.async_write_ha_state()
class ServiceStartedSensor(SensorEntity, BaseSysMonEntity):
_attr_device_class = SensorDeviceClass.TIMESTAMP
def __init__(self, coordinator: SysMonCoordinator, hostname: str, service_name: str):
super().__init__(coordinator, hostname)
self._hostname = hostname
self._service_name = service_name
self._attr_native_value = None
LOGGER.debug(f"{self.name_log}: created.")
@property
def name_log(self) -> str:
return f"ServiceStartedSensor[{self._hostname}][{self._service_name}]"
@property
def name(self) -> str:
return f"SysMon {self._hostname} service started: {self._service_name}"
@callback
def _handle_coordinator_update(self) -> None:
if not self.coordinator.data[self._hostname][STATE_IS_CONNECTED]:
if self._attr_native_value is not None:
self._attr_native_value = None
self.async_write_ha_state()
LOGGER.debug(f"{self.name_log}: polled state. Agent is NOT connected. Received: {self.coordinator.data[self._hostname]}.")
return
LOGGER.debug(f"{self.name_log}: polled state. Agent IS connected. Received: {self.coordinator.data[self._hostname]}.")
state_data: api.MonitoringStats = self.coordinator.data[self._hostname][STATE_DATA]
data: Optional[api.MonitoredService] = state_data.service_by_name(self._service_name)
if data is None or data.main_started.ToSeconds() == 0:
self._attr_native_value = None
else:
timestamp = data.main_started.ToDatetime()
timezone = datetime.datetime.now().astimezone().tzinfo
self._attr_native_value = timestamp.replace(tzinfo=timezone)
self.async_write_ha_state()
class ServiceStoppedSensor(SensorEntity, BaseSysMonEntity):
_attr_device_class = SensorDeviceClass.TIMESTAMP
def __init__(self, coordinator: SysMonCoordinator, hostname: str, service_name: str):
super().__init__(coordinator, hostname)
self._hostname = hostname
self._service_name = service_name
self._attr_native_value = None
LOGGER.debug(f"{self.name_log}: created.")
@property
def name_log(self) -> str:
return f"ServiceStoppedSensor[{self._hostname}][{self._service_name}]"
@property
def name(self) -> str:
return f"SysMon {self._hostname} service stopped: {self._service_name}"
@callback
def _handle_coordinator_update(self) -> None:
if not self.coordinator.data[self._hostname][STATE_IS_CONNECTED]:
if self._attr_native_value is not None:
self._attr_native_value = None
self.async_write_ha_state()
LOGGER.debug(f"{self.name_log}: polled state. Agent is NOT connected. Received: {self.coordinator.data[self._hostname]}.")
return
LOGGER.debug(f"{self.name_log}: polled state. Agent IS connected. Received: {self.coordinator.data[self._hostname]}.")
state_data: api.MonitoringStats = self.coordinator.data[self._hostname][STATE_DATA]
data: Optional[api.MonitoredService] = state_data.service_by_name(self._service_name)
if data is None or data.main_exited.ToSeconds() == 0:
self._attr_native_value = None
else:
timestamp = data.main_exited.ToDatetime()
timezone = datetime.datetime.now().astimezone().tzinfo
self._attr_native_value = timestamp.replace(tzinfo=timezone)
self.async_write_ha_state()
class ServiceResultStatusSensor(BinarySensorEntity, BaseSysMonEntity):
_attr_device_class = BinarySensorDeviceClass.PROBLEM
def __init__(self, coordinator: SysMonCoordinator, hostname: str, service_name: str):
super().__init__(coordinator, hostname)
self._hostname = hostname
self._service_name = service_name
self._attr_is_on = None
LOGGER.debug(f"{self.name_log}: created.")
@property
def name_log(self) -> str:
return f"ServiceResultStatusSensor[{self._hostname}][{self._service_name}]"
@property
def name(self) -> str:
return f"SysMon {self._hostname} service failed: {self._service_name}"
@callback
def _handle_coordinator_update(self) -> None:
if not self.coordinator.data[self._hostname][STATE_IS_CONNECTED]:
if self._attr_is_on is not None:
self._attr_is_on = None
self.async_write_ha_state()
LOGGER.debug(f"{self.name_log}: polled state. Agent is NOT connected. Received: {self.coordinator.data[self._hostname]}.")
return
LOGGER.debug(f"{self.name_log}: polled state. Agent IS connected. Received: {self.coordinator.data[self._hostname]}.")
state_data: api.MonitoringStats = self.coordinator.data[self._hostname][STATE_DATA]
data: Optional[api.MonitoredService] = state_data.service_by_name(self._service_name)
if data is None:
self._attr_is_on = None
else:
self._attr_is_on = not data.result_success
self.async_write_ha_state()

@ -0,0 +1 @@
Subproject commit 675f35f2a60e8b4f93baf03aa1746b96d3f86f29

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
colorlog==6.7.0
homeassistant==2024.1.0
pip>=21.0,<23.2
ruff==0.0.292

20
scripts/develop Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
# Create config dir if not present
if [[ ! -d "${PWD}/config" ]]; then
mkdir -p "${PWD}/config"
hass --config "${PWD}/config" --script ensure_config
fi
# Set the path to custom_components
## This let's us have the structure we want <root>/custom_components/sys_mon
## while at the same time have Home Assistant configuration inside <root>/config
## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
# Start Home Assistant
hass --config "${PWD}/config" --debug

7
scripts/lint Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
ruff check . --fix

7
scripts/setup Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
python3 -m pip install --requirement requirements.txt

15
scripts/sys-mon-agent Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
if [[ ! -d "${PWD}/custom_components/sys_mon/sys_mon_agent/venv" ]]; then
python -m venv "${PWD}/custom_components/sys_mon/sys_mon_agent/venv"
source "${PWD}/custom_components/sys_mon/sys_mon_agent/venv/bin/activate"
pip install -r "${PWD}/custom_components/sys_mon/sys_mon_agent/requirements.txt"
else
source "${PWD}/custom_components/sys_mon/sys_mon_agent/venv/bin/activate"
fi
python "${PWD}/custom_components/sys_mon/sys_mon_agent/sys-mon-agent.py" -p 8202