Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/modules/common/modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pymodbus.client.sync import ModbusTcpClient, ModbusSerialClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.transaction import ModbusSocketFramer
from urllib3.util import parse_url

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -197,12 +198,13 @@ def __init__(self,
address: str,
port: int = 502,
sleep_after_connect: Optional[int] = 0,
framer: type[ModbusSocketFramer] = ModbusSocketFramer,
**kwargs):
parsed_url = parse_url(address)
host = parsed_url.host
if parsed_url.port is not None:
port = parsed_url.port
super().__init__(ModbusTcpClient(host, port, **kwargs), address, port, sleep_after_connect)
super().__init__(ModbusTcpClient(host, port, framer, **kwargs), address, port, sleep_after_connect)


class ModbusSerialClient_(ModbusClient):
Expand Down
5 changes: 5 additions & 0 deletions packages/modules/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
module.BinaryPayloadDecoder = Mock()
sys.modules['pymodbus.payload'] = module

module = type(sys)('pymodbus.transaction')
module.ModbusSocketFramer = Mock()
module.ModbusRtuFramer = Mock()
sys.modules['pymodbus.transaction'] = module

module = type(sys)('socketserver')
module.TCPServer = Mock()
sys.modules['socketserver'] = module
Expand Down
Empty file.
Empty file.
50 changes: 50 additions & 0 deletions packages/modules/devices/avm/avm/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Optional
from helpermodules.auto_str import auto_str
from modules.common.component_setup import ComponentSetup

from ..vendor import vendor_descriptor


@auto_str
class AvmConfiguration:
def __init__(self,
ip_address: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
session_id: Optional[str] = None,
session_mtime: Optional[str] = None) -> None:
self.ip_address = ip_address
self.username = username
self.password = password
self.session_id = session_id # don't show in UI
self.session_mtime = session_mtime # don't show in UI


@auto_str
class Avm:
def __init__(self,
name: str = "AVM Fritz!Box",
type: str = "avm",
id: int = 0,
configuration: AvmConfiguration = None) -> None:
self.name = name
self.type = type
self.vendor = vendor_descriptor.configuration_factory().type
self.id = id
self.configuration = configuration or AvmConfiguration()


@auto_str
class AvmCounterConfiguration:
def __init__(self, name: Optional[str] = None):
self.name = name


@auto_str
class AvmCounterSetup(ComponentSetup[AvmCounterConfiguration]):
def __init__(self,
name: str = "Avm Zähler",
type: str = "counter",
id: int = 0,
configuration: AvmCounterConfiguration = None) -> None:
super().__init__(name, type, id, configuration or AvmCounterConfiguration())
48 changes: 48 additions & 0 deletions packages/modules/devices/avm/avm/counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
from xml.etree.ElementTree import Element

from modules.common.abstract_device import AbstractCounter
from modules.common.component_state import CounterState
from modules.common.component_type import ComponentDescriptor
from modules.common.fault_state import ComponentInfo, FaultState
from modules.common.store import get_counter_value_store
from modules.devices.avm.avm.config import AvmCounterSetup


class AvmCounter(AbstractCounter):
def __init__(self, component_config: AvmCounterSetup) -> None:
self.component_config = component_config

def initialize(self) -> None:
self.store = get_counter_value_store(self.component_config.id)
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))

def update(self, deviceListElementTree: Element):
for device in deviceListElementTree:
name = device.find("name").text
if name == self.component_config.configuration.name:
presentText = device.find("present").text
if presentText != '1':
continue

powermeterBlock = device.find("powermeter")
if powermeterBlock is not None:
# AVM returns mW, convert to W here
power = float(powermeterBlock.find("power").text)/1000
# AVM returns mV, convert to V here
voltageInfo = powermeterBlock.find("voltage")
if voltageInfo is not None:
voltages = [float(voltageInfo.text)/1000, 0, 0]
# AVM returns Wh
imported = powermeterBlock.find("energy").text

counter_state = CounterState(
imported=imported,
exported=0,
power=power,
voltages=voltages
)
self.store.set(counter_state)


component_descriptor = ComponentDescriptor(configuration_factory=AvmCounterSetup)
84 changes: 84 additions & 0 deletions packages/modules/devices/avm/avm/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
import hashlib
import logging
import time
from typing import Iterable
import xml.etree.ElementTree as ET

from dataclass_utils._dataclass_asdict import asdict
from helpermodules.pub import Pub
from modules.common import req
from modules.common.abstract_device import DeviceDescriptor
from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater
from modules.devices.avm.avm.config import Avm, AvmCounterSetup
from modules.devices.avm.avm.counter import AvmCounter

log = logging.getLogger(__name__)

INVALID_SESSIONID = "0000000000000000"


def create_device(device_config: Avm):
def create_counter_component(component_config: AvmCounterSetup):
return AvmCounter(component_config)

def update_components(components: Iterable[AvmCounter]):
if (device_config.configuration.session_id is None or
device_config.configuration.session_mtime is None or
time.time() - device_config.configuration.session_mtime > 300):
device_config.configuration.session_mtime = time.time()
device_config.configuration.session_id = get_session_id()
Pub().pub(f"openWB/set/system/device/{device_config.id}/config", asdict(device_config))

response = req.get_http_session().get(
f"http://{device_config.configuration.ip_address}/webservices/homeautoswitch.lua?sid="
f"{device_config.configuration.session_id}&switchcmd=getdevicelistinfos")
deviceListElementTree = ET.fromstring(response.text.strip())

for component in components:
component.update(deviceListElementTree)

def get_session_id():
# checking existing sessionID
response = req.get_http_session().post(f"http://{device_config.configuration.ip_address}/login_sid.lua")
challengeResponse = ET.fromstring(response.content)
session_id = challengeResponse.find('SID').text
if session_id != INVALID_SESSIONID:
return
blockTimeXML = challengeResponse.find('BlockTime')
if blockTimeXML is not None and int(blockTimeXML.text) > 0:
raise Exception("Durch Anmeldefehler in der Vergangenheit ist der Zugang zur FRITZ!Box "
f"noch für {blockTimeXML.text} Sekunden gesperrt.")

# last sessionID was invalid, performing new challenge-response authentication
challenge = challengeResponse.find('Challenge').text
m = hashlib.md5()
m.update((f"{challenge}-{device_config.configuration.password}").encode('utf-16le'))
hashedPassword = m.hexdigest()

data = {
'username': device_config.configuration.username,
'response': challenge + "-" + hashedPassword
}
try:
response = req.get_http_session().post(
f"http://{device_config.configuration.ip_address}/login_sid.lua", data=data, timeout=5)
session_info = ET.fromstring(response.content)
session_id = session_info.find('SID').text
except Exception:
session_id = None
raise Exception("Anmeldung fehlgeschlagen, bitte Benutzername und Passwort überprüfen. Anmeldung für "
f"die nächsten {session_info.find('BlockTime').text} Sekunden durch FRITZ!Box-Webinterface "
"gesperrt.")
return session_id

return ConfigurableDevice(
device_config=device_config,
component_factory=ComponentFactoryByType(
counter=create_counter_component,
),
component_updater=MultiComponentUpdater(update_components)
)


device_descriptor = DeviceDescriptor(configuration_factory=Avm)
14 changes: 14 additions & 0 deletions packages/modules/devices/avm/vendor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pathlib import Path

from modules.common.abstract_device import DeviceDescriptor
from modules.devices.vendors import VendorGroup


class Vendor:
def __init__(self):
self.type = Path(__file__).parent.name
self.vendor = "AVM Fritz!Box"
self.group = VendorGroup.VENDORS.value


vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor)
Empty file.
Empty file.
41 changes: 41 additions & 0 deletions packages/modules/devices/mystrom/mystrom/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Optional
from helpermodules.auto_str import auto_str
from modules.common.component_setup import ComponentSetup

from ..vendor import vendor_descriptor


@auto_str
class MystromConfiguration:
def __init__(self, ip_address: Optional[str] = None):
self.ip_address = ip_address


@auto_str
class Mystrom:
def __init__(self,
name: str = "mystrom",
type: str = "mystrom",
id: int = 0,
configuration: MystromConfiguration = None) -> None:
self.name = name
self.type = type
self.vendor = vendor_descriptor.configuration_factory().type
self.id = id
self.configuration = configuration or MystromConfiguration()


@auto_str
class MystromCounterConfiguration:
def __init__(self):
pass


@auto_str
class MystromCounterSetup(ComponentSetup[MystromCounterConfiguration]):
def __init__(self,
name: str = "mystrom Zähler",
type: str = "counter",
id: int = 0,
configuration: MystromCounterConfiguration = None) -> None:
super().__init__(name, type, id, configuration or MystromCounterConfiguration())
43 changes: 43 additions & 0 deletions packages/modules/devices/mystrom/mystrom/counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
from requests import Session
from typing import TypedDict, Any
from modules.common.abstract_device import AbstractCounter
from modules.common.component_state import CounterState
from modules.common.component_type import ComponentDescriptor
from modules.common.fault_state import ComponentInfo, FaultState
from modules.common.simcount import SimCounter
from modules.common.store import get_counter_value_store
from modules.devices.mystrom.mystrom.config import MystromCounterSetup


class KwargsDict(TypedDict):
device_id: int
ip_address: str


class MystromCounter(AbstractCounter):
def __init__(self, component_config: MystromCounterSetup, **kwargs: Any) -> None:
self.component_config = component_config
self.kwargs: KwargsDict = kwargs

def initialize(self) -> None:
self.__device_id: int = self.kwargs['device_id']
self.ip_address: str = self.kwargs['ip_address']
self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug")
self.store = get_counter_value_store(self.component_config.id)
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))

def update(self, session: Session):
resp = session.get(f"http://{self.ip_address}/report").json()
power = resp["power"]
imported, exported = self.sim_counter.sim_count(power)

counter_state = CounterState(
imported=imported,
exported=exported,
power=power,
)
self.store.set(counter_state)


component_descriptor = ComponentDescriptor(configuration_factory=MystromCounterSetup)
35 changes: 35 additions & 0 deletions packages/modules/devices/mystrom/mystrom/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import logging

from modules.common import req
from modules.common.abstract_device import DeviceDescriptor
from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, IndependentComponentUpdater
from modules.devices.mystrom.mystrom.config import Mystrom, MystromCounterSetup
from modules.devices.mystrom.mystrom.counter import MystromCounter

log = logging.getLogger(__name__)


def create_device(device_config: Mystrom):
session = None

def create_counter_component(component_config: MystromCounterSetup):
return MystromCounter(component_config,
device_id=device_config.id,
ip_address=device_config.configuration.ip_address)

def initializer():
nonlocal session
session = req.get_http_session()

return ConfigurableDevice(
device_config=device_config,
initializer=initializer,
component_factory=ComponentFactoryByType(
counter=create_counter_component,
),
component_updater=IndependentComponentUpdater(lambda component: component.update(session))
)


device_descriptor = DeviceDescriptor(configuration_factory=Mystrom)
14 changes: 14 additions & 0 deletions packages/modules/devices/mystrom/vendor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pathlib import Path

from modules.common.abstract_device import DeviceDescriptor
from modules.devices.vendors import VendorGroup


class Vendor:
def __init__(self):
self.type = Path(__file__).parent.name
self.vendor = "mystrom"
self.group = VendorGroup.VENDORS.value


vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor)
Empty file.
Empty file.
Loading