Skip to content

Commit

Permalink
Merge pull request #342 from TWilkin/341.ComputerDevice-ping
Browse files Browse the repository at this point in the history
#341/#285/#297 Perform ping when turning on computer device
  • Loading branch information
TWilkin committed Jun 5, 2023
2 parents b5817b6 + 0cc0b1c commit 932f739
Show file tree
Hide file tree
Showing 26 changed files with 363 additions and 229 deletions.
3 changes: 2 additions & 1 deletion PowerPi.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@
"type": "shell",
"presentation": {
"reveal": "always"
}
},
"problemMatcher": []
}
],
"inputs": [
Expand Down
2 changes: 1 addition & 1 deletion common/python/powerpi_common/device/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ async def __change_power_handler(
if success is not False:
self.state = new_status
else:
self.log_info(f'Failed to {new_status} device {self}')
self.log_info(f'Failed to turn {new_status} device {self}')
except Exception as ex:
self.log_exception(ex)
self.state = DeviceStatus.UNKNOWN
Expand Down
2 changes: 1 addition & 1 deletion common/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "powerpi-common"
version = "0.3.2"
version = "0.3.3"
description = "PowerPi Common Python Library"
license = "GPL-3.0-only"
authors = ["TWilkin <4322355+TWilkin@users.noreply.github.com>"]
Expand Down
6 changes: 2 additions & 4 deletions common/python/tests/event/test_consumer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from datetime import datetime

import pytest
from powerpi_common_test.base import BaseTest
from pytest_mock import MockerFixture

from powerpi_common.event.consumer import EventConsumer
from powerpi_common_test.base import BaseTest


class EventHandlerImpl:
Expand Down Expand Up @@ -40,7 +38,7 @@ def create_subject(self, mocker: MockerFixture):
)

@pytest.mark.parametrize('timestamp,expected', [
(datetime.now().timestamp() * 1000, 2),
(1685910908 * 1000, 2),
(None, 2),
(0, 0)
])
Expand Down
18 changes: 15 additions & 3 deletions controllers/network/network_controller/device/computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(
mac: str,
ip: str = None,
hostname: str = None,
delay: int = 10,
**kwargs
):
# pylint: disable=too-many-arguments
Expand All @@ -32,6 +33,7 @@ def __init__(

self.__mac_address = mac
self.__network_address = ip if ip is not None else hostname
self.__delay = delay

@property
def mac_address(self):
Expand All @@ -42,9 +44,9 @@ def network_address(self):
return self.__network_address

async def poll(self):
result = await async_ping(self.__network_address, count=4, interval=0.2, timeout=2)
is_alive = await self.__is_alive(4)

new_state = DeviceStatus.ON if result.is_alive else DeviceStatus.OFF
new_state = DeviceStatus.ON if is_alive else DeviceStatus.OFF

if new_state != self.state:
self.state = new_state
Expand All @@ -53,8 +55,18 @@ async def _turn_on(self):
for _ in range(0, 4):
send_magic_packet(self.__mac_address)

await sleep(0.2)
await sleep(self.__delay)

if await self.__is_alive():
return True

return False

async def _turn_off(self):
# do nothing as this will be handled by the shutdown service running on that computer
return False

async def __is_alive(self, count=1):
result = await async_ping(self.__network_address, count=count, interval=0.2, timeout=2)

return result.is_alive
2 changes: 1 addition & 1 deletion controllers/network/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "network_controller"
version = "0.1.0"
version = "0.1.1"
description = "PowerPi Network Controller"
license = "GPL-3.0-only"
authors = ["TWilkin <4322355+TWilkin@users.noreply.github.com>"]
Expand Down
45 changes: 37 additions & 8 deletions controllers/network/tests/device/test_computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,41 @@ async def test_poll_ping(
assert subject.state == state

@pytest.mark.asyncio
async def test_turn_on(self, subject: ComputerDevice):
@pytest.mark.parametrize('attempts,success', [(1, True), (3, True), (5, False)])
async def test_turn_on(
self,
subject: ComputerDevice,
mocker: MockerFixture,
attempts: int,
success: bool
):
# pylint: disable=arguments-differ

assert subject.state == DeviceStatus.UNKNOWN

is_alive = [i == attempts for i in range(1, attempts + 1)]

host = mocker.MagicMock()
type(host).is_alive = PropertyMock(side_effect=is_alive)

mocker.patch(
'network_controller.device.computer.async_ping',
return_value=host
)

with patch('network_controller.device.computer.send_magic_packet') as wol:
await subject.turn_on()

wol.assert_has_calls([
call('00:00:00:00:00'),
call('00:00:00:00:00'),
call('00:00:00:00:00'),
calls = [
call('00:00:00:00:00')
])
for _ in range(0, min(4, attempts))
]
wol.assert_has_calls(calls)

assert subject.state == DeviceStatus.ON
if success:
assert subject.state == DeviceStatus.ON
else:
assert subject.state == DeviceStatus.UNKNOWN

@pytest.mark.asyncio
async def test_turn_off(self, subject: ComputerDevice):
Expand All @@ -92,6 +113,14 @@ async def test_change_message(

mocker.patch.object(powerpi_config, 'message_age_cutoff', 120)

host = mocker.Mock()
type(host).is_alive = PropertyMock(return_value=True)

mocker.patch(
'network_controller.device.computer.async_ping',
return_value=host
)

message = {
'state': 'on',
'timestamp': int(datetime.utcnow().timestamp() * 1000)
Expand All @@ -111,6 +140,6 @@ def subject(
):
return ComputerDevice(
powerpi_config, powerpi_logger, powerpi_mqtt_client,
mac='00:00:00:00:00', hostname='mycomputer.home',
mac='00:00:00:00:00', hostname='mycomputer.home', delay=0.1,
name='computer', poll_frequency=120
)
6 changes: 6 additions & 0 deletions controllers/node/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ RUN ln -s ../../common/python/.venv .venv \
FROM python:3.9.16-slim AS run-image
LABEL description="PowerPi Node Controller"

RUN apt-get update \
&& apt-get install -y libcap2-bin

ENV PATH="/usr/src/app/venv/bin:$PATH"
ENV TZ="UTC"

# ensure the controller can run ping
RUN setcap cap_net_raw+ep /usr/local/bin/python3.9

RUN groupadd -g 997 gpio \
&& groupadd -g 998 i2c \
&& adduser --disabled-password powerpi \
Expand Down
4 changes: 3 additions & 1 deletion controllers/node/node_controller/device/container.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dependency_injector import providers

from node_controller.config import NodeConfig
from node_controller.pijuice import PiJuiceImpl
from node_controller.pwm_fan import PWMFanController
Expand Down Expand Up @@ -54,7 +55,8 @@ def add_devices(container):
config=container.common.config,
logger=container.common.logger,
mqtt_client=container.common.mqtt_client,
service_provider=container.common.device.service_provider,
pijuice_interface_factory=device_container.pijuice_interface.provider,
pwm_fan_controller_factory=device_container.pwm_fan_controller.provider,
shutdown=container.shutdown
)
)
Expand Down
15 changes: 9 additions & 6 deletions controllers/node/node_controller/device/local_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
from asyncio import sleep
from typing import List, TypedDict, Union

from node_controller.pijuice import PiJuiceInterface
from node_controller.pwm_fan import PWMFanCurve, PWMFanInterface
from node_controller.services import ShutdownService
from dependency_injector import providers
from powerpi_common.config import Config
from powerpi_common.device import Device, DeviceStatus
from powerpi_common.device.mixin import InitialisableMixin, PollableMixin
from powerpi_common.logger import Logger
from powerpi_common.mqtt import MQTTClient
from powerpi_common.sensor.mixin import BatteryMixin

from node_controller.pijuice import PiJuiceInterface
from node_controller.pwm_fan import PWMFanCurve, PWMFanInterface
from node_controller.services import ShutdownService

PiJuiceConfig = TypedDict(
'PiJuiceConfig',
{
Expand Down Expand Up @@ -40,7 +42,8 @@ def __init__(
config: Config,
logger: Logger,
mqtt_client: MQTTClient,
service_provider,
pijuice_interface_factory: providers.Factory,
pwm_fan_controller_factory: providers.Factory,
shutdown: ShutdownService,
pijuice: Union[PiJuiceConfig, None] = None,
pwm_fan: Union[PWMFanConfig, None] = None,
Expand All @@ -52,7 +55,7 @@ def __init__(
BatteryMixin.__init__(self)

if pijuice is not None:
self.__pijuice: PiJuiceInterface = service_provider.pijuice_interface()
self.__pijuice: PiJuiceInterface = pijuice_interface_factory()

# set the config with defaults
self.__pijuice_config = PiJuiceConfig({
Expand All @@ -68,7 +71,7 @@ def __init__(
self.__pijuice_config = None

if pwm_fan is not None:
self.__pwm_fan: PWMFanInterface = service_provider.pwm_fan_controller(
self.__pwm_fan: PWMFanInterface = pwm_fan_controller_factory(
pijuice=self.__pijuice
)

Expand Down
2 changes: 1 addition & 1 deletion controllers/node/node_controller/device/remote_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(
self.__ip = ip

async def poll(self):
result = await async_ping(self.__ip, count=4, interval=0.2, timeout=2, privileged=False)
result = await async_ping(self.__ip, count=4, interval=0.2, timeout=2)

new_state = DeviceStatus.ON if result.is_alive else DeviceStatus.OFF

Expand Down
2 changes: 1 addition & 1 deletion controllers/node/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "node_controller"
version = "0.1.1"
version = "0.1.2"
description = "PowerPi Node Controller"
license = "GPL-3.0-only"
authors = ["TWilkin <4322355+TWilkin@users.noreply.github.com>"]
Expand Down
6 changes: 6 additions & 0 deletions controllers/node/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# pylint: disable=unused-import

import pytest
from powerpi_common_test.fixture import (powerpi_config, powerpi_logger,
powerpi_mqtt_client,
powerpi_mqtt_producer)
12 changes: 6 additions & 6 deletions controllers/node/tests/device/test_factory.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from unittest.mock import MagicMock, PropertyMock

import pytest
from node_controller.device.factory import NodeDeviceFactory
from powerpi_common.device.types import DeviceConfigType
from pytest_mock import MockerFixture

from node_controller.device.factory import NodeDeviceFactory


class TestNodeDeviceFactory:
@pytest.mark.parametrize('name', ['node123', 'NODE123', 'Node123'])
Expand Down Expand Up @@ -37,12 +38,11 @@ def test_build_sensor(self, subject: NodeDeviceFactory):
assert result == 'sensor'

@pytest.fixture
def mock_config(self, mocker: MockerFixture):
config = mocker.Mock()

type(config).node_hostname = PropertyMock(return_value='NODE123')
def mock_config(self, powerpi_config):
type(powerpi_config).node_hostname = PropertyMock(
return_value='NODE123')

return config
return powerpi_config

@pytest.fixture
def mock_service_provider(self, mocker: MockerFixture):
Expand Down
Loading

0 comments on commit 932f739

Please sign in to comment.