Skip to content

Commit e9788a5

Browse files
authored
Merge pull request #2 from ManticSic/auto_discover
Auto discover
2 parents e64f60f + bd86f16 commit e9788a5

File tree

13 files changed

+151
-68
lines changed

13 files changed

+151
-68
lines changed

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM python:3.7
22

3-
ENV DEVICE unknown
4-
ENV INTERVAL 5
3+
ENV FETCH_INTERVAL 5
4+
ENV DISCOVER_INTERVAL 30
55
ENV LOGSTASH_HOST unknown
66
ENV LOGSTASH_PORT 5000
77

@@ -10,8 +10,8 @@ WORKDIR /usr/src/app
1010
COPY requierments.txt .
1111
COPY main.py .
1212
COPY setup setup
13+
COPY services services
1314
COPY models models
14-
COPY collecting collecting
1515

1616
COPY entry.sh .
1717
RUN chmod +x entry.sh

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Kasa Reporter
22

3-
Small pyton program to send emeter data of kasa devices to logstash
3+
Small python program to send emeter data of kasa devices to logstash
44

55
## Build
66

@@ -12,16 +12,15 @@ $ docker build -t kasa-reporter .
1212
## Run
1313

1414
```
15-
$ docker run --network host --env DEVICE=ip-or-hostname --env LOGSTASH_HOST=ip-or-hostname --name kasa-reporter kasa-reporter
15+
$ docker run --network host --env LOGSTASH_HOST=ip-or-hostname --name kasa-reporter kasa-reporter
1616
```
1717

1818
## Arguments and environment variables
1919

2020
Argument|Environment Variable|Type|Mandatory|Default|Description
2121
:---|---:|---:|---:|---:|---:
22-
`-d`, `--device`|`DEVICE`|String|Yes|n/a|Ip or hostname of the device.
23-
`-t`, `--type`|n/a|string|No|`plug`|Type of the device. Only the type `plug` is currently supported.
24-
`-i`, `--interval`|`INTERVAL`|Integer|No|`5`|Interval of requesting emeter data in seconds.
22+
`--fetch-interval`|`FETCH_INTERVAL`|Integer|No|`5`|Interval of requesting emeter data in seconds.
23+
`--discover-interval`|`DISCOVER_INTERVAL`|Integer|No|`30`|Interval of discovering devices in network 255.255.255.255.
2524
`--logstash-host`|`LOGSTASH_HOST`|String|Yes|n/a|Ip or hostname of the logstash server.
2625
`--logstash-port`|`LOGSTASH_PORT`|Integer|No|`5000`|Port of the logstash server.
2726

collecting/kasa.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

docker-compose.yml renamed to docker-compose.example.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ services:
55
image: kasa-reporter
66
restart: always
77
environment:
8-
DEVICE: "unknown"
9-
INTERVAL: 5
8+
FETCH_INTERVAL: 5
9+
DISCOVER_INTERVAL: 30
1010
LOGSTASH_HOST: "unknown"
1111
LOGSTASH_PORT: 5000
1212
network_mode: "host"

main.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@
22
import sys
33
import setup.log
44
import setup.configuration
5-
6-
from time import sleep
7-
from collecting.kasa import KasaCollector
5+
from services.discoveryservice import DiscoveryService
6+
from services.emeterservice import EmeterService
87

98
configuration = setup.configuration.get_configuration()
10-
logger = setup.log.get_logger()
9+
logger = setup.log.get_logger('main')
10+
11+
discovery_service = DiscoveryService()
12+
emeter_service = EmeterService(discovery_service)
1113

1214

1315
async def main() -> None:
1416
logger.info('Start fetching data.')
15-
collector = KasaCollector(configuration)
16-
await collector.setup()
1717

18-
while True:
19-
data = await collector.fetch()
20-
logger.info('Successfully fetched data.', data=data.__dict__)
21-
sleep(configuration.interval)
18+
discovery_service.start()
19+
emeter_service.start()
2220

2321

2422
if __name__ == '__main__':

models/SmartDeviceSpare.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from kasa import SmartDevice
2+
3+
4+
class SmartDeviceSpare:
5+
def __init__(self, device: SmartDevice):
6+
self.host = device.host
7+
self.device_id = device.device_id
8+
self.alias = device.alias

models/configuration.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
class Configuration:
22

33
def __init__(self, args):
4-
self.device = args.device
5-
self.interval = args.interval
6-
self.device_type = args.type
4+
self.fetch_interval = args.fetch_interval
5+
self.discover_interval = args.discover_interval
76
self.logstash_host = args.logstash_host
87
self.logstash_port = args.logstash_port
File renamed without changes.

services/backgroundservice.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import asyncio
2+
from threading import Thread
3+
from typing import Union
4+
5+
6+
class CancellationToken:
7+
is_cancellation_requested: bool = False
8+
9+
def request_cancellation(self):
10+
self.is_cancellation_requested = True
11+
12+
13+
class BackgroundService:
14+
cancellation_token: Union[CancellationToken, None] = None
15+
thread: Union[Thread, None] = None
16+
17+
def __init__(self, job):
18+
self.job = job
19+
20+
def start(self):
21+
if self.cancellation_token is not None:
22+
raise Exception('Service is already started.')
23+
24+
self.cancellation_token = CancellationToken()
25+
self.thread = Thread(target=self.__run)
26+
self.thread.start()
27+
28+
def stop(self):
29+
if self.cancellation_token is None:
30+
raise Exception('Service is not running.')
31+
32+
self.cancellation_token.request_cancellation()
33+
34+
def __run(self):
35+
asyncio.run(self.__job())
36+
37+
async def __job(self):
38+
if self.job is None:
39+
raise Exception('No job defined.')
40+
41+
await self.job()

services/discoveryservice.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import List
2+
from kasa import SmartDevice
3+
from kasa import Discover
4+
from time import sleep
5+
6+
from models.SmartDeviceSpare import SmartDeviceSpare
7+
from services.backgroundservice import BackgroundService
8+
from setup.log import get_logger
9+
10+
logger = get_logger('DiscoveryService')
11+
12+
13+
class DiscoveryService(BackgroundService):
14+
devices: List[SmartDevice] = []
15+
16+
def __init__(self):
17+
super().__init__(self.__job_implementation)
18+
19+
async def __job_implementation(self):
20+
while not self.cancellation_token.is_cancellation_requested:
21+
result = await Discover.discover()
22+
23+
device_dict = dict(filter(lambda device: device[1].has_emeter, result.items()))
24+
self.devices = list(map(lambda device: device[1], device_dict.items()))
25+
26+
devices_spare = list(map(lambda device: SmartDeviceSpare(device), self.devices))
27+
logger.info(f'Devices discovered.', devices=devices_spare.__repr__())
28+
29+
sleep(10)
30+
31+
self.cancellation_token = None
32+
33+
def get_devices(self):
34+
return self.devices

services/emeterservice.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from time import sleep
2+
from kasa import SmartDevice
3+
from models.SmartDeviceSpare import SmartDeviceSpare
4+
from models.customeemterstatus import CustomEmeterStatus
5+
from services.backgroundservice import BackgroundService
6+
from services.discoveryservice import DiscoveryService
7+
from setup.log import get_logger
8+
9+
logger = get_logger('EmeterService')
10+
11+
12+
class EmeterService(BackgroundService):
13+
def __init__(self, discovery_service: DiscoveryService):
14+
super().__init__(self.__job_implementation)
15+
self.discovery_service = discovery_service
16+
17+
async def __job_implementation(self):
18+
while not self.cancellation_token.is_cancellation_requested:
19+
devices = self.discovery_service.devices
20+
for device in devices:
21+
try:
22+
data = await self.__fetch(device)
23+
logger.info('Fetched data.', data=data.__dict__, device=SmartDeviceSpare(device).__dict__)
24+
except Exception:
25+
# todo log and format exception
26+
logger.error('Failed to fetch data.', device=SmartDeviceSpare(device).__dict__)
27+
sleep(5)
28+
29+
self.cancellation_token = None
30+
31+
async def __fetch(self, device: SmartDevice) -> CustomEmeterStatus:
32+
await device.update()
33+
34+
emeter_status = await device.get_emeter_realtime()
35+
today = device.emeter_today
36+
this_month = device.emeter_this_month
37+
38+
return CustomEmeterStatus(
39+
emeter_status['voltage_mv'],
40+
emeter_status['current_ma'],
41+
emeter_status['power_mw'],
42+
emeter_status['total_wh'],
43+
today,
44+
this_month
45+
)

setup/configuration.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
# configure argument parser
55
parser = ArgumentParser()
66

7-
parser.add_argument('-d', '--device', type=str, required=True) # device ip or hostname
8-
parser.add_argument('-t', '--type', type=str, choices=['plug'], default='plug') # device type
9-
parser.add_argument('-i', '--interval', type=int, default=5, required=False) # fetch interval in seconds
7+
parser.add_argument('--fetch-interval', type=int, default=5, required=False) # fetch interval in seconds
8+
parser.add_argument('--discover-interval', type=int, default=30, required=False) # fetch interval in seconds
109
parser.add_argument('--logstash-host', type=str, required=True) # logstash ip or hostname
1110
parser.add_argument('--logstash-port', type=int, default=5000, required=False) # logstash port
1211

setup/log.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,5 @@ def __add_configuration(logger: logging.Logger, method_name: str, event_dict: Ev
8383
)
8484

8585

86-
def get_logger() -> Any:
87-
return structlog.get_logger('kasa-reporter')
86+
def get_logger(name: str) -> Any:
87+
return structlog.get_logger(name)

0 commit comments

Comments
 (0)