Refine location
This commit is contained in:
@@ -77,7 +77,6 @@ class KasaPowerStripDataExporter(interface.IPrometheusDataExporter):
|
|||||||
|
|
||||||
class TapoT315DataExporter(interface.IPrometheusDataExporter):
|
class TapoT315DataExporter(interface.IPrometheusDataExporter):
|
||||||
def __init__(self, ip, username, password, devName, devNameInEnglish=""):
|
def __init__(self, ip, username, password, devName, devNameInEnglish=""):
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
||||||
self.devIp = ip
|
self.devIp = ip
|
||||||
self.devUsername = username
|
self.devUsername = username
|
||||||
self.devPassword = password
|
self.devPassword = password
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
LOGFORMAT_RICH = '%(thread)d %(message)s'
|
LOGFORMAT_RICH = '%(message)s'
|
||||||
richHandler = RichHandler(
|
richHandler = RichHandler(
|
||||||
rich_tracebacks=True,
|
rich_tracebacks=True,
|
||||||
)
|
)
|
||||||
|
|||||||
14
dockerfiles/dataExporter/Dockerfile
Normal file
14
dockerfiles/dataExporter/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.13
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y lm-sensors smartmontools && pip install \
|
||||||
|
python-kasa \
|
||||||
|
rich \
|
||||||
|
schedule \
|
||||||
|
prometheus-client
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY code/prometheus_data_exporter.py ./
|
||||||
|
COPY code/awinlib ./awinlib
|
||||||
|
|
||||||
|
CMD []
|
||||||
|
ENTRYPOINT ["python3", "/app/prometheus_data_exporter.py", "-f", "/app/.secret"]
|
||||||
6
dockerfiles/dataExporter/buildImage.sh
Executable file
6
dockerfiles/dataExporter/buildImage.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cp ../../prometheus_data_exporter.py code/
|
||||||
|
cp -r ../../awinlib code/
|
||||||
|
|
||||||
|
sudo docker build --debug -t awin/sensors:latest .
|
||||||
239
dockerfiles/dataExporter/code/awinlib/KasaSmartPowerStrip.py
Normal file
239
dockerfiles/dataExporter/code/awinlib/KasaSmartPowerStrip.py
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import socket
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
from builtins import bytes
|
||||||
|
|
||||||
|
class SmartPowerStrip(object):
|
||||||
|
|
||||||
|
def __init__(self, ip, device_id=None, timeout=2.0, protocol='tcp'):
|
||||||
|
self.ip = ip
|
||||||
|
self.port = 9999
|
||||||
|
self.protocol = protocol
|
||||||
|
self.device_id = device_id
|
||||||
|
self.sys_info = None
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
self.sys_info = self.get_system_info()['system']['get_sysinfo']
|
||||||
|
|
||||||
|
if not self.device_id:
|
||||||
|
self.device_id = self.sys_info['deviceId']
|
||||||
|
|
||||||
|
def set_wifi_credentials(self, ssid, psk, key_type='3'):
|
||||||
|
'''
|
||||||
|
:param ssid: router ssid
|
||||||
|
:param psk: router passkey
|
||||||
|
:param key_type: 3 is WPA2, 2 might be WPA and 1 might be WEP?
|
||||||
|
:return: command response
|
||||||
|
'''
|
||||||
|
|
||||||
|
wifi_command = '{"netif":{"set_stainfo":{"ssid":"' + ssid + '","password":"' + \
|
||||||
|
psk + '","key_type":' + key_type + '}}}'
|
||||||
|
|
||||||
|
return self.send_command(wifi_command, self.protocol)
|
||||||
|
|
||||||
|
def set_cloud_server_url(self, server_url=''):
|
||||||
|
|
||||||
|
server_command = '{"cnCloud":{"set_server_url":{"server":"' + server_url + '"}}}'
|
||||||
|
|
||||||
|
return self.send_command(server_command, self.protocol)
|
||||||
|
|
||||||
|
def get_system_info(self):
|
||||||
|
|
||||||
|
return self._udp_send_command('{"system":{"get_sysinfo":{}}}')
|
||||||
|
|
||||||
|
def get_realtime_energy_info(self, plug_num=None, plug_name=None):
|
||||||
|
|
||||||
|
plug_id = self._get_plug_id(plug_num=plug_num, plug_name=plug_name)
|
||||||
|
|
||||||
|
energy_command = '{"context":{"child_ids":["' + plug_id + '"]},"emeter":{"get_realtime":{}}}'
|
||||||
|
|
||||||
|
response = self.send_command(energy_command, self.protocol)
|
||||||
|
|
||||||
|
realtime_energy_data = response['emeter']['get_realtime']
|
||||||
|
|
||||||
|
return realtime_energy_data
|
||||||
|
|
||||||
|
def get_historical_energy_info(self, month, year, plug_num=None, plug_name=None):
|
||||||
|
|
||||||
|
plug_id = self._get_plug_id(plug_num=plug_num, plug_name=plug_name)
|
||||||
|
|
||||||
|
energy_command = '{"context":{"child_ids":["' + plug_id + '"]},' + \
|
||||||
|
'"emeter":{"get_daystat":{"month": ' + month + ',"year":' + year + '}}}'
|
||||||
|
|
||||||
|
response = self.send_command(energy_command, self.protocol)
|
||||||
|
|
||||||
|
historical_energy_data = response['emeter']['get_daystat']['day_list']
|
||||||
|
|
||||||
|
return historical_energy_data
|
||||||
|
|
||||||
|
def toggle_relay_leds(self, state):
|
||||||
|
|
||||||
|
state_int = self._get_plug_state_int(state, reverse=True)
|
||||||
|
|
||||||
|
led_command = '{"system":{"set_led_off":{"off":' + str(state_int) + '}}}'
|
||||||
|
|
||||||
|
return self.send_command(led_command, self.protocol)
|
||||||
|
|
||||||
|
def set_plug_name(self, plug_num, plug_name):
|
||||||
|
|
||||||
|
plug_id = self._get_plug_id(plug_num=plug_num)
|
||||||
|
|
||||||
|
set_name_command = '{"context":{"child_ids":["' + plug_id + \
|
||||||
|
'"]},"system":{"set_dev_alias":{"alias":"' + plug_name + '"}}}'
|
||||||
|
|
||||||
|
return self.send_command(set_name_command, self.protocol)
|
||||||
|
|
||||||
|
def get_plug_info(self, plug_num):
|
||||||
|
|
||||||
|
target_plug = [plug for plug in self.sys_info['children'] if plug['id'] == str(int(plug_num)-1).zfill(2)]
|
||||||
|
|
||||||
|
return target_plug
|
||||||
|
|
||||||
|
# toggle multiple plugs by id or name
|
||||||
|
def toggle_plugs(self, state, plug_num_list=None, plug_name_list=None):
|
||||||
|
|
||||||
|
state_int = self._get_plug_state_int(state)
|
||||||
|
|
||||||
|
plug_id_list_str = self._get_plug_id_list_str(plug_num_list=plug_num_list, plug_name_list=plug_name_list)
|
||||||
|
|
||||||
|
all_relay_command = '{"context":{"child_ids":' + plug_id_list_str + '},' + \
|
||||||
|
'"system":{"set_relay_state":{"state":' + str(state_int) + '}}}'
|
||||||
|
|
||||||
|
return self.send_command(all_relay_command, self.protocol)
|
||||||
|
|
||||||
|
# toggle a single plug
|
||||||
|
def toggle_plug(self, state, plug_num=None, plug_name=None):
|
||||||
|
|
||||||
|
state_int = self._get_plug_state_int(state)
|
||||||
|
|
||||||
|
plug_id = self._get_plug_id(plug_num=plug_num, plug_name=plug_name)
|
||||||
|
|
||||||
|
relay_command = '{"context":{"child_ids":["' + plug_id + '"]},' + \
|
||||||
|
'"system":{"set_relay_state":{"state":' + str(state_int) + '}}}'
|
||||||
|
|
||||||
|
return self.send_command(relay_command, self.protocol)
|
||||||
|
|
||||||
|
def reboot(self, delay=1):
|
||||||
|
reboot_command = '{"system":{"reboot":{"delay":' + str(delay) + '}}}'
|
||||||
|
return self.send_command(reboot_command, self.protocol)
|
||||||
|
|
||||||
|
# manually send a command
|
||||||
|
def send_command(self, command, protocol='tcp'):
|
||||||
|
|
||||||
|
if protocol == 'tcp':
|
||||||
|
return self._tcp_send_command(command)
|
||||||
|
elif protocol == 'udp':
|
||||||
|
return self._udp_send_command(command)
|
||||||
|
else:
|
||||||
|
raise ValueError("Protocol must be 'tcp' or 'udp'")
|
||||||
|
|
||||||
|
def _get_plug_state_int(self, state, reverse=False):
|
||||||
|
|
||||||
|
if state.lower() == 'on':
|
||||||
|
if reverse:
|
||||||
|
state_int = 0
|
||||||
|
else:
|
||||||
|
state_int = 1
|
||||||
|
elif state.lower() == 'off':
|
||||||
|
if reverse:
|
||||||
|
state_int = 1
|
||||||
|
else:
|
||||||
|
state_int = 0
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid state, must be 'on' or 'off'")
|
||||||
|
|
||||||
|
return state_int
|
||||||
|
|
||||||
|
# create a string with a list of plug_ids that can be inserted directly into a command
|
||||||
|
def _get_plug_id_list_str(self, plug_num_list=None, plug_name_list=None):
|
||||||
|
|
||||||
|
plug_id_list = []
|
||||||
|
|
||||||
|
if plug_num_list:
|
||||||
|
for plug_num in plug_num_list:
|
||||||
|
|
||||||
|
# add as str to remove the leading u
|
||||||
|
plug_id_list.append(str(self._get_plug_id(plug_num=plug_num)))
|
||||||
|
|
||||||
|
elif plug_name_list:
|
||||||
|
|
||||||
|
for plug_name in plug_name_list:
|
||||||
|
# add as str to remove the leading u
|
||||||
|
plug_id_list.append(str(self._get_plug_id(plug_name=plug_name)))
|
||||||
|
|
||||||
|
# convert to double quotes and turn the whole list into a string
|
||||||
|
plug_id_list_str = str(plug_id_list).replace("'", '"')
|
||||||
|
|
||||||
|
return plug_id_list_str
|
||||||
|
|
||||||
|
# get the plug child_id to be used with commands
|
||||||
|
def _get_plug_id(self, plug_num=None, plug_name=None):
|
||||||
|
|
||||||
|
if plug_num and self.device_id:
|
||||||
|
plug_id = self.device_id + str(plug_num-1).zfill(2)
|
||||||
|
|
||||||
|
elif plug_name and self.sys_info:
|
||||||
|
target_plug = [plug for plug in self.sys_info['children'] if plug['alias'] == plug_name]
|
||||||
|
if target_plug:
|
||||||
|
plug_id = self.device_id + target_plug[0]['id']
|
||||||
|
else:
|
||||||
|
raise ValueError('Unable to find plug with name ' + plug_name)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unable to find plug. Provide a valid plug_num or plug_name')
|
||||||
|
|
||||||
|
return plug_id
|
||||||
|
|
||||||
|
def _tcp_send_command(self, command):
|
||||||
|
|
||||||
|
sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock_tcp.settimeout(self.timeout)
|
||||||
|
sock_tcp.connect((self.ip, self.port))
|
||||||
|
|
||||||
|
sock_tcp.send(self._encrypt_command(command))
|
||||||
|
|
||||||
|
data = sock_tcp.recv(2048)
|
||||||
|
sock_tcp.close()
|
||||||
|
|
||||||
|
# the first 4 chars are the length of the command so can be excluded
|
||||||
|
return json.loads(self._decrypt_command(data[4:]))
|
||||||
|
|
||||||
|
def _udp_send_command(self, command):
|
||||||
|
|
||||||
|
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
client_socket.settimeout(self.timeout)
|
||||||
|
|
||||||
|
addr = (self.ip, self.port)
|
||||||
|
|
||||||
|
client_socket.sendto(self._encrypt_command(command, prepend_length=False), addr)
|
||||||
|
|
||||||
|
data, server = client_socket.recvfrom(2048)
|
||||||
|
|
||||||
|
return json.loads(self._decrypt_command(data))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _encrypt_command(string, prepend_length=True):
|
||||||
|
|
||||||
|
key = 171
|
||||||
|
result = b''
|
||||||
|
|
||||||
|
# when sending get_sysinfo using udp the length of the command is not needed but
|
||||||
|
# with all other commands using tcp it is
|
||||||
|
if prepend_length:
|
||||||
|
result = struct.pack(">I", len(string))
|
||||||
|
|
||||||
|
for i in bytes(string.encode('latin-1')):
|
||||||
|
a = key ^ i
|
||||||
|
key = a
|
||||||
|
result += bytes([a])
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _decrypt_command(string):
|
||||||
|
|
||||||
|
key = 171
|
||||||
|
result = b''
|
||||||
|
for i in bytes(string):
|
||||||
|
a = key ^ i
|
||||||
|
key = i
|
||||||
|
result += bytes([a])
|
||||||
|
return result.decode('latin-1')
|
||||||
121
dockerfiles/dataExporter/code/awinlib/PrometheusDataExporter.py
Normal file
121
dockerfiles/dataExporter/code/awinlib/PrometheusDataExporter.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import asyncio
|
||||||
|
import weakref
|
||||||
|
|
||||||
|
import awinlib.cpufantemp as cpufantemp
|
||||||
|
import awinlib.hdd as hdd
|
||||||
|
import awinlib.PrometheusDataExporterInterface as interface
|
||||||
|
import awinlib.tapo
|
||||||
|
import awinlib.utilities
|
||||||
|
from awinlib.utilities import log
|
||||||
|
|
||||||
|
|
||||||
|
class HddTempDataExporter(interface.IPrometheusDataExporter):
|
||||||
|
DEFAULT_ERROR_HDD_TEMP = -273
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
exportData = []
|
||||||
|
|
||||||
|
hddTempDict = hdd.getHddTemp()
|
||||||
|
for hddKey in sorted(hddTempDict.keys()):
|
||||||
|
hddName = hddKey.replace("/dev/", "")
|
||||||
|
hddType = hddTempDict[hddKey].get("TYPE", "")
|
||||||
|
hddTemperature = int(hddTempDict[hddKey].get("temp", HddTempDataExporter.DEFAULT_ERROR_HDD_TEMP))
|
||||||
|
# uuid = hddTempDict[hddKey].get("UUID", "").replace("-", "_")
|
||||||
|
# partUuid = hddTempDict[hddKey].get("PARTUUID", "")
|
||||||
|
gaugeName = f"{hddName}_{hddType}_celsius"
|
||||||
|
gaugeDescription = f"HDD temperature of {gaugeName}"
|
||||||
|
|
||||||
|
exportData.append({
|
||||||
|
'name': gaugeName,
|
||||||
|
'description': gaugeDescription,
|
||||||
|
'value': hddTemperature
|
||||||
|
})
|
||||||
|
return exportData
|
||||||
|
|
||||||
|
|
||||||
|
class CpuFanTempDataExporter(interface.IPrometheusDataExporter):
|
||||||
|
def export(self):
|
||||||
|
exportData = []
|
||||||
|
fanTempDict = cpufantemp.getCpuTempAndFanSpeed()
|
||||||
|
|
||||||
|
if "fan" not in fanTempDict:
|
||||||
|
return exportData
|
||||||
|
|
||||||
|
for fanKey in sorted(fanTempDict["fan"].keys()):
|
||||||
|
fanSpeed = fanTempDict["fan"][fanKey]
|
||||||
|
|
||||||
|
gaugeName = f"{fanKey}_rpm"
|
||||||
|
gaugeDescription = f"Fan RPM of {gaugeName}"
|
||||||
|
|
||||||
|
exportData.append({
|
||||||
|
'name': gaugeName,
|
||||||
|
'description': gaugeDescription,
|
||||||
|
'value': fanSpeed
|
||||||
|
})
|
||||||
|
return exportData
|
||||||
|
|
||||||
|
|
||||||
|
class KasaPowerStripDataExporter(interface.IPrometheusDataExporter):
|
||||||
|
def __init__(self, areaName, ip):
|
||||||
|
self.powerStrip = awinlib.tapo.KasaPowerStrip(areaName, ip)
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
exportData = []
|
||||||
|
powerStripData = self.powerStrip.collectData()
|
||||||
|
for keyId in sorted(powerStripData.keys()):
|
||||||
|
for gaugeName in powerStripData[keyId].keys():
|
||||||
|
gaugeDesc = powerStripData[keyId][gaugeName]["description"]
|
||||||
|
gaugeValue = powerStripData[keyId][gaugeName]["value"]
|
||||||
|
|
||||||
|
exportData.append({
|
||||||
|
'name': gaugeName,
|
||||||
|
'description': gaugeDesc,
|
||||||
|
'value': gaugeValue
|
||||||
|
})
|
||||||
|
return exportData
|
||||||
|
|
||||||
|
|
||||||
|
class TapoT315DataExporter(interface.IPrometheusDataExporter):
|
||||||
|
def __init__(self, ip, username, password, devName, devNameInEnglish=""):
|
||||||
|
self.devIp = ip
|
||||||
|
self.devUsername = username
|
||||||
|
self.devPassword = password
|
||||||
|
self.devName = devName
|
||||||
|
self.devNameInEnglish = devNameInEnglish
|
||||||
|
self.devInstance = awinlib.tapo.TplinkT315(ip, username, password, devName)
|
||||||
|
if awinlib.utilities.syncRun(self.devInstance.connect()) is True:
|
||||||
|
log(f'Connected to T315({ip}, {devName})')
|
||||||
|
self.__finalizer = weakref.finalize(self, self._del, self)
|
||||||
|
else:
|
||||||
|
self.devInstance = None
|
||||||
|
raise ConnectionError(f"Failed to connect to T315({ip}, {devName})")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _del(this):
|
||||||
|
if this.devInstance:
|
||||||
|
log('Disconnecting from T315')
|
||||||
|
awinlib.utilities.syncRun(this.devInstance.disconnect())
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
exportData = []
|
||||||
|
|
||||||
|
temperatureValue = awinlib.utilities.syncRun(self.devInstance.getTemperature())
|
||||||
|
if temperatureValue is not None:
|
||||||
|
gaugeName = f"{self.normalizeGaugename(self.devNameInEnglish)}_temperature_celsius" if self.devNameInEnglish != "" else "temperature_celsius"
|
||||||
|
gaugeDescription = "Temperature of T315"
|
||||||
|
exportData.append({
|
||||||
|
'name': gaugeName,
|
||||||
|
'description': gaugeDescription,
|
||||||
|
'value': temperatureValue
|
||||||
|
})
|
||||||
|
|
||||||
|
humidityValue = awinlib.utilities.syncRun(self.devInstance.getHumidity())
|
||||||
|
if humidityValue is not None:
|
||||||
|
gaugeName = f"{self.normalizeGaugename(self.devNameInEnglish)}_humidity_percent" if self.devNameInEnglish != "" else "humidity_percent"
|
||||||
|
gaugeDescription = "Humidity of T315"
|
||||||
|
exportData.append({
|
||||||
|
'name': gaugeName,
|
||||||
|
'description': gaugeDescription,
|
||||||
|
'value': humidityValue
|
||||||
|
})
|
||||||
|
return exportData
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import abc
|
||||||
|
|
||||||
|
|
||||||
|
class IPrometheusDataExporter:
|
||||||
|
@abc.abstractmethod
|
||||||
|
def export(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def normalizeGaugename(self, originalName):
|
||||||
|
return originalName.replace("-", "_").lower()
|
||||||
15
dockerfiles/dataExporter/code/awinlib/__init__.py
Normal file
15
dockerfiles/dataExporter/code/awinlib/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
|
LOGFORMAT_RICH = '%(message)s'
|
||||||
|
richHandler = RichHandler(
|
||||||
|
rich_tracebacks=True,
|
||||||
|
)
|
||||||
|
richHandler.setFormatter(logging.Formatter(LOGFORMAT_RICH))
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
handlers=[
|
||||||
|
richHandler,
|
||||||
|
]
|
||||||
|
)
|
||||||
54
dockerfiles/dataExporter/code/awinlib/cpufantemp.py
Normal file
54
dockerfiles/dataExporter/code/awinlib/cpufantemp.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .utilities import runCmd
|
||||||
|
|
||||||
|
|
||||||
|
def getCpuTempAndFanSpeed():
|
||||||
|
if os.name == "nt":
|
||||||
|
return {}
|
||||||
|
|
||||||
|
cmd = "sensors"
|
||||||
|
ret = runCmd(cmd).split("\n")
|
||||||
|
|
||||||
|
fanTempDict = {
|
||||||
|
"cpu": {},
|
||||||
|
"fan": {}
|
||||||
|
}
|
||||||
|
for line in ret:
|
||||||
|
if line.startswith("Core"):
|
||||||
|
line = line.split(":")
|
||||||
|
if len(line) > 1:
|
||||||
|
cpuIndex, tempString = line[0], line[1]
|
||||||
|
tempString = tempString.split(" ")
|
||||||
|
tempString = [x for x in tempString if x != ""]
|
||||||
|
if len(tempString) > 1:
|
||||||
|
temp = float(tempString[0].replace("+", "").replace("°C", ""))
|
||||||
|
else:
|
||||||
|
temp = -999.9
|
||||||
|
fanTempDict["cpu"][cpuIndex] = temp
|
||||||
|
|
||||||
|
elif line.startswith("fan"):
|
||||||
|
line = line.split(":")
|
||||||
|
if len(line) > 1:
|
||||||
|
fanId, fanNumber = line[0], line[1]
|
||||||
|
fanNumber = fanNumber.split(" ")
|
||||||
|
fanNumber = [x for x in fanNumber if x != ""]
|
||||||
|
if len(fanNumber) > 1:
|
||||||
|
fanSpeed = int(fanNumber[0])
|
||||||
|
else:
|
||||||
|
fanSpeed = 0
|
||||||
|
|
||||||
|
if fanSpeed > 0:
|
||||||
|
fanTempDict["fan"][fanId] = fanSpeed
|
||||||
|
return fanTempDict
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
fanTempDict = getCpuTempAndFanSpeed()
|
||||||
|
print(f"{json.dumps(fanTempDict, indent=4)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
49
dockerfiles/dataExporter/code/awinlib/hdd.py
Normal file
49
dockerfiles/dataExporter/code/awinlib/hdd.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from .utilities import runCmd
|
||||||
|
|
||||||
|
|
||||||
|
def findHddIdInNt():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def findHddIdInPosix():
|
||||||
|
hddDict = {}
|
||||||
|
results = runCmd("blkid").split("\n")
|
||||||
|
for line in results:
|
||||||
|
if line == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
hddInfoList = line.split(" ")
|
||||||
|
if len(hddInfoList) > 1 and hddInfoList[0].startswith("/dev/sd"):
|
||||||
|
hddName = hddInfoList[0].replace(":", "")
|
||||||
|
hddDict[hddName] = {}
|
||||||
|
for hddInfoline in hddInfoList[1:]:
|
||||||
|
kvList = hddInfoline.split("=")
|
||||||
|
if len(kvList) > 1:
|
||||||
|
hddDict[hddName][kvList[0].replace("\"", "")] = kvList[1].replace("\"", "")
|
||||||
|
return hddDict
|
||||||
|
|
||||||
|
|
||||||
|
def findHddId():
|
||||||
|
hddDict = {}
|
||||||
|
if os.name == "nt":
|
||||||
|
hddDict = findHddIdInNt()
|
||||||
|
elif os.name == "posix":
|
||||||
|
hddDict = findHddIdInPosix()
|
||||||
|
return hddDict
|
||||||
|
|
||||||
|
|
||||||
|
def findHddTemp(hddDict):
|
||||||
|
for hddName in sorted(hddDict.keys()):
|
||||||
|
cmd = f"smartctl -A {hddName} | grep Temperature_Celsius"
|
||||||
|
hddTempString = runCmd(cmd)
|
||||||
|
hddTempList = hddTempString.split(" ")
|
||||||
|
hddTempList = [item for item in hddTempList if item != ""]
|
||||||
|
if len(hddTempList) > 9:
|
||||||
|
hddDict[hddName]["temp"] = int(hddTempList[9])
|
||||||
|
|
||||||
|
|
||||||
|
def getHddTemp():
|
||||||
|
hddDict = findHddId()
|
||||||
|
findHddTemp(hddDict)
|
||||||
|
return hddDict
|
||||||
132
dockerfiles/dataExporter/code/awinlib/tapo.py
Normal file
132
dockerfiles/dataExporter/code/awinlib/tapo.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
from kasa import Discover
|
||||||
|
from kasa.exceptions import KasaException
|
||||||
|
|
||||||
|
from . import KasaSmartPowerStrip
|
||||||
|
from .utilities import log as log
|
||||||
|
|
||||||
|
|
||||||
|
class KasaPowerStrip:
|
||||||
|
def __init__(self, areaName, ip):
|
||||||
|
self.areaName = areaName
|
||||||
|
self.ip = ip
|
||||||
|
try:
|
||||||
|
self.powerStrip = KasaSmartPowerStrip.SmartPowerStrip(ip)
|
||||||
|
except:
|
||||||
|
self.powerStrip = None
|
||||||
|
|
||||||
|
def collectData(self):
|
||||||
|
dataDict = {}
|
||||||
|
devSysInfo = self.powerStrip.get_system_info()
|
||||||
|
for c in devSysInfo['system']['get_sysinfo']['children']:
|
||||||
|
try:
|
||||||
|
cid = int(c['id']) + 1
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# aliasName = c['alias'].encode('latin1').decode('UTF-8')
|
||||||
|
gaugeName_Current_ma = f"{self.areaName}_{cid}_current_ma"
|
||||||
|
gaugeName_Current_ma_desc = f"Current(milliampere) of {self.areaName}:{cid}"
|
||||||
|
gaugeName_Voltage_mv = f"{self.areaName}_{cid}_voltage_mv"
|
||||||
|
gaugeName_Voltage_mv_desc = f"Voltage(millivolt) of {self.areaName}:{cid}"
|
||||||
|
gaugeName_Power_mw = f"{self.areaName}_{cid}_power_mw"
|
||||||
|
gaugeName_Power_mw_desc = f"Power(milliwatt) of {self.areaName}:{cid}"
|
||||||
|
gaugeName_TotalWh = f"{self.areaName}_{cid}_total_wh"
|
||||||
|
gaugeName_TotalWh_desc = f"Total watt-hour of {self.areaName}:{cid}"
|
||||||
|
|
||||||
|
## Get Data
|
||||||
|
realtimeInfo = self.powerStrip.get_realtime_energy_info(plug_num=cid)
|
||||||
|
realTime_current_ma = realtimeInfo.get("current_ma", 0)
|
||||||
|
realTime_voltage_mv = realtimeInfo.get("voltage_mv", 0)
|
||||||
|
realTime_power_mw = realtimeInfo.get("power_mw", 0)
|
||||||
|
realTime_total_wh = realtimeInfo.get("total_wh", 0)
|
||||||
|
|
||||||
|
# log(f"Set {gaugeName_Current_ma}: {realTime_current_ma}")
|
||||||
|
# log(f"Set {gaugeName_Voltage_mv}: {realTime_voltage_mv}")
|
||||||
|
# log(f"Set {gaugeName_Power_mw}: {realTime_power_mw}")
|
||||||
|
# log(f"Set {gaugeName_TotalWh}: {realTime_total_wh}")
|
||||||
|
|
||||||
|
dataDict[cid] = {
|
||||||
|
gaugeName_Current_ma: {
|
||||||
|
"description": gaugeName_Current_ma_desc,
|
||||||
|
"value": realTime_current_ma,
|
||||||
|
},
|
||||||
|
gaugeName_Voltage_mv: {
|
||||||
|
"description": gaugeName_Voltage_mv_desc,
|
||||||
|
"value": realTime_voltage_mv,
|
||||||
|
},
|
||||||
|
gaugeName_Power_mw: {
|
||||||
|
"description": gaugeName_Power_mw_desc,
|
||||||
|
"value": realTime_power_mw,
|
||||||
|
},
|
||||||
|
gaugeName_TotalWh: {
|
||||||
|
"description": gaugeName_TotalWh_desc,
|
||||||
|
"value": realTime_total_wh,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataDict
|
||||||
|
|
||||||
|
|
||||||
|
class TplinkT315:
|
||||||
|
def __init__(self, ip, username, password, devName):
|
||||||
|
self.ip = ip
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.devName = devName
|
||||||
|
self.hubDev = None
|
||||||
|
self.t315Dev = None
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
try:
|
||||||
|
self.hubDev = await Discover.discover_single(self.ip, username=self.username, password=self.password)
|
||||||
|
await self.hubDev.update()
|
||||||
|
except KasaException as e:
|
||||||
|
self.hubDev = None
|
||||||
|
log(f"Hub Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.t315Dev = self.hubDev.get_plug_by_name(self.devName)
|
||||||
|
await self.t315Dev.update()
|
||||||
|
except KasaException as e:
|
||||||
|
self.t315Dev = None
|
||||||
|
log(f"Device Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def getTemperature(self):
|
||||||
|
temperatureValue = await self.getProperty("temperature")
|
||||||
|
if temperatureValue is None:
|
||||||
|
log("temperatureValue is None")
|
||||||
|
return None
|
||||||
|
|
||||||
|
temperatureValue = float(temperatureValue)
|
||||||
|
return temperatureValue
|
||||||
|
|
||||||
|
async def getHumidity(self):
|
||||||
|
humidityValue = await self.getProperty("humidity")
|
||||||
|
if humidityValue is None:
|
||||||
|
log("humidityValue is None")
|
||||||
|
return None
|
||||||
|
|
||||||
|
humidityValue = float(humidityValue)
|
||||||
|
return humidityValue
|
||||||
|
|
||||||
|
async def getProperty(self, propName):
|
||||||
|
if self.t315Dev is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
await self.hubDev.update()
|
||||||
|
await self.t315Dev.update()
|
||||||
|
|
||||||
|
featuresOfTempDev = self.t315Dev.features
|
||||||
|
if propName not in featuresOfTempDev:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return featuresOfTempDev[propName].value
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
if self.hubDev is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.hubDev.disconnect()
|
||||||
77
dockerfiles/dataExporter/code/awinlib/utilities.py
Normal file
77
dockerfiles/dataExporter/code/awinlib/utilities.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from rich import print as pp
|
||||||
|
|
||||||
|
|
||||||
|
def runCmd(cmdString):
|
||||||
|
result = subprocess.run(cmdString, stdout=subprocess.PIPE, shell=True).stdout.decode('utf-8')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def syncRun(coroutine):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
# coroutine = funcInstace()
|
||||||
|
return loop.run_until_complete(coroutine)
|
||||||
|
|
||||||
|
|
||||||
|
def datetimeNow():
|
||||||
|
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def loadJson(fileanme):
|
||||||
|
try:
|
||||||
|
with open(fileanme, "r") as f:
|
||||||
|
secrets = json.load(f)
|
||||||
|
|
||||||
|
return secrets
|
||||||
|
except FileNotFoundError:
|
||||||
|
log(f"File not found: {fileanme}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def log2(message, *args):
|
||||||
|
frame = inspect.currentframe().f_back
|
||||||
|
class_name = frame.f_locals.get('self', '').__class__.__name__
|
||||||
|
func_name = frame.f_code.co_name
|
||||||
|
line_no = frame.f_lineno
|
||||||
|
caller = f"{class_name}::{func_name},{line_no}"
|
||||||
|
prefix = f"[{datetimeNow()}|{caller}]"
|
||||||
|
pp(f"{prefix} {message}", end='')
|
||||||
|
for arg in args:
|
||||||
|
pp(arg, end='')
|
||||||
|
pp("")
|
||||||
|
|
||||||
|
|
||||||
|
def log(message, *args):
|
||||||
|
log = logging.getLogger()
|
||||||
|
log.info(message)
|
||||||
|
|
||||||
|
|
||||||
|
def logD(message, *args):
|
||||||
|
log = logging.getLogger()
|
||||||
|
log.debug(message)
|
||||||
|
|
||||||
|
|
||||||
|
def logI(message, *args):
|
||||||
|
log = logging.getLogger()
|
||||||
|
log.info(message)
|
||||||
|
|
||||||
|
|
||||||
|
def logW(message, *args):
|
||||||
|
log = logging.getLogger()
|
||||||
|
log.warning(message)
|
||||||
|
|
||||||
|
|
||||||
|
def logE(message, *args):
|
||||||
|
log = logging.getLogger()
|
||||||
|
log.error(message)
|
||||||
|
|
||||||
|
|
||||||
|
def logC(message, *args):
|
||||||
|
log = logging.getLogger()
|
||||||
|
log.critical(message)
|
||||||
88
dockerfiles/dataExporter/code/prometheus_data_exporter.py
Normal file
88
dockerfiles/dataExporter/code/prometheus_data_exporter.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
import schedule
|
||||||
|
from prometheus_client import Gauge, start_http_server
|
||||||
|
|
||||||
|
from awinlib.PrometheusDataExporter import (
|
||||||
|
CpuFanTempDataExporter,
|
||||||
|
HddTempDataExporter,
|
||||||
|
KasaPowerStripDataExporter,
|
||||||
|
TapoT315DataExporter,
|
||||||
|
)
|
||||||
|
from awinlib.utilities import datetimeNow, loadJson, log
|
||||||
|
|
||||||
|
ENABLE_EXPORTER = True
|
||||||
|
DEFAULT_PORT = 8087
|
||||||
|
DEFAULT_POLLING_INTERVAL = 60
|
||||||
|
DEFAULT_SECRET_FILEPATH = "~/.secret"
|
||||||
|
DEFAULT_CONFIGS = {
|
||||||
|
"port": DEFAULT_PORT,
|
||||||
|
"polling_interval": DEFAULT_POLLING_INTERVAL,
|
||||||
|
"secret_filepath": DEFAULT_SECRET_FILEPATH
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def job_5s(gaugeDict, exporters):
|
||||||
|
exportData(gaugeDict, exporters)
|
||||||
|
|
||||||
|
|
||||||
|
def job_60s(gaugeDict, exporters):
|
||||||
|
exportData(gaugeDict, exporters)
|
||||||
|
|
||||||
|
|
||||||
|
def exportData(gaugeDict, exporters):
|
||||||
|
for exporter in exporters:
|
||||||
|
data = exporter.export()
|
||||||
|
for d in data:
|
||||||
|
gaugeName = d["name"]
|
||||||
|
gaugeDesc = d["description"]
|
||||||
|
gaugeValue = d["value"]
|
||||||
|
log(f"Set {gaugeName}: {gaugeValue}")
|
||||||
|
if gaugeName not in gaugeDict:
|
||||||
|
gaugeDict[gaugeName] = Gauge(gaugeName, gaugeDesc)
|
||||||
|
|
||||||
|
if ENABLE_EXPORTER:
|
||||||
|
gaugeDict[gaugeName].set(gaugeValue)
|
||||||
|
|
||||||
|
|
||||||
|
def main(configs):
|
||||||
|
log(f"Start: {datetimeNow()}")
|
||||||
|
secrect = loadJson(configs.get("secret_filepath", DEFAULT_POLLING_INTERVAL))
|
||||||
|
if secrect is None:
|
||||||
|
log(f'Cannot read secret file({configs.get("secret_filepath", DEFAULT_POLLING_INTERVAL)})')
|
||||||
|
return
|
||||||
|
|
||||||
|
hddTempExporter = HddTempDataExporter()
|
||||||
|
cpuFanTempExporter = CpuFanTempDataExporter()
|
||||||
|
powerStripLivingroomExporter = KasaPowerStripDataExporter("livingroom", "192.168.1.203")
|
||||||
|
powerStripAtticExporter = KasaPowerStripDataExporter("attic", "192.168.1.203")
|
||||||
|
t315Atticexporter = TapoT315DataExporter(secrect["ip"], secrect["username"], secrect["password"], "鐵皮屋-溫濕度感測器", "attic-sensors")
|
||||||
|
gaugeDict = {}
|
||||||
|
|
||||||
|
schedule.every( 5).seconds.do(exportData, gaugeDict=gaugeDict, exporters=(hddTempExporter, cpuFanTempExporter, powerStripLivingroomExporter, powerStripAtticExporter))
|
||||||
|
schedule.every(60).seconds.do(exportData, gaugeDict=gaugeDict, exporters=(t315Atticexporter,))
|
||||||
|
|
||||||
|
## Start Prometheus server
|
||||||
|
prometheusServerPort = configs.get("port", DEFAULT_PORT)
|
||||||
|
log(f"Start Prometheus server on port {prometheusServerPort}")
|
||||||
|
start_http_server(prometheusServerPort) if ENABLE_EXPORTER else None
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log("User stopped process.")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT, help="The port of exportor")
|
||||||
|
parser.add_argument("-i", "--polling_interval", type=int, default=DEFAULT_POLLING_INTERVAL, help="Polling interval for collect HDD temperature")
|
||||||
|
parser.add_argument("-f", "--secrect_file", default=".secret", help="The file that contains the secrets")
|
||||||
|
args = parser.parse_args()
|
||||||
|
DEFAULT_CONFIGS["port"] = args.port
|
||||||
|
DEFAULT_CONFIGS["polling_interval"] = args.polling_interval
|
||||||
|
DEFAULT_CONFIGS["secret_filepath"] = args.secrect_file
|
||||||
|
main(DEFAULT_CONFIGS)
|
||||||
9
dockerfiles/dataExporter/run.sh
Executable file
9
dockerfiles/dataExporter/run.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
sudo docker run -it --rm \
|
||||||
|
--privileged \
|
||||||
|
-v /etc/sensors.d:/etc/sensors.d:ro \
|
||||||
|
-v /etc/sensors3.conf:/etc/sensors3.conf:ro \
|
||||||
|
-v /dev:/dev \
|
||||||
|
-v /home/awin/.secret/tplink:/app/.secret \
|
||||||
|
awin/sensors:latest
|
||||||
@@ -13,7 +13,7 @@ from awinlib.PrometheusDataExporter import (
|
|||||||
from awinlib.utilities import datetimeNow, loadJson, log
|
from awinlib.utilities import datetimeNow, loadJson, log
|
||||||
|
|
||||||
ENABLE_EXPORTER = True
|
ENABLE_EXPORTER = True
|
||||||
DEFAULT_PORT = 8089
|
DEFAULT_PORT = 8087
|
||||||
DEFAULT_POLLING_INTERVAL = 60
|
DEFAULT_POLLING_INTERVAL = 60
|
||||||
DEFAULT_SECRET_FILEPATH = "~/.secret"
|
DEFAULT_SECRET_FILEPATH = "~/.secret"
|
||||||
DEFAULT_CONFIGS = {
|
DEFAULT_CONFIGS = {
|
||||||
|
|||||||
Reference in New Issue
Block a user