diff --git a/classDiagram.txt b/classDiagram.txt index 2950c4d836cc96a8872e944415a0e04d765f6527..b300566ab043bc546e76db998d541718cf8a80c9 100644 --- a/classDiagram.txt +++ b/classDiagram.txt @@ -1,20 +1,51 @@ @startuml + class Participant{ - secretKey + publicKey + + publicIP + + + updateRemoteParticipants(new_rps) + + findOffers(power, time) + + findRequests(power, time) } class RemoteParticipant{ + publicKey + + publicIP + + + updateExchanges(new_exs) +} +abstract class Exchange{ + power: Watt + pricePerWatt: Euro } -class Trade -class Exchange +class Trade { + + power: Watt + + pricePerWatt: Euro + + signature_offer + + signature_request + + + verify_trade() +} + class Offer class Request Exchange <|-- Offer Exchange <|-- Request -Exchange "1..n" - Trade + +Participant "0..n" - RemoteParticipant + +RemoteParticipant "0..n" - Exchange + +Trade "1..1" - "n..1" Offer + +Trade "1..1" - "n..1" Request + +Trade "2" - Trade + + @enduml \ No newline at end of file diff --git a/exchange.py b/exchange.py index f5f9e1833b41a49e4327dafe7f4e902c17318393..c205277b019da927533318eac5f891662a311ccd 100644 --- a/exchange.py +++ b/exchange.py @@ -1,13 +1,56 @@ #from main import ureg,Q_ import locale import time +from datetime import datetime,timedelta class Trade: pass class Exchange: - def __init__(self, p: float, ppw: float): - self.__timestamp: time.time_ns() + def __init__(self,pk: bytes, p: float, ppw: float): + self.pubicKey: bytes = pk + self.timestamp: int = time.time_ns() + self.__isActive: bool = False #self.__power = Q_(p, ureg.watt) self.__pricePerWatt = locale.currency(ppw) self.__trades: set[Trade] = set() - self.__sig: bytes #signed exchange hash when becoming active \ No newline at end of file + self.__sig: bytes + + + def activate(self) -> None: + self.__isActive = True + + def getActive(self) -> bool: + return self.__isActive + + def is_min_now(self) -> bool: + now = datetime.now() + + # Calculate the first second of the current minute + start_of_minute = datetime(now.year, now.month, now.day, now.hour, now.minute, 0) + start_of_minute_unix = int(start_of_minute.timestamp()) + + # Calculate the last second of the current minute + end_of_minute = start_of_minute + timedelta(seconds=59) + end_of_minute_unix = int(end_of_minute.timestamp()) + + if start_of_minute_unix <= self.timestamp <= end_of_minute_unix: + return True + else: + return False + + def is_min_next(self) -> bool: + now = datetime.now() + + # Calculate the first second of the next minute + start_of_minute = datetime(now.year, now.month, now.day, now.hour, now.minute, 0) + start_of_minute += timedelta(minutes=1) + start_of_minute_unix = int(start_of_minute.timestamp()) + + # Calculate the last second of the next minute + end_of_minute = start_of_minute + timedelta(seconds=59) + end_of_minute_unix = int(end_of_minute.timestamp()) + + if start_of_minute_unix <= self.timestamp <= end_of_minute_unix: + return True + else: + return False \ No newline at end of file diff --git a/offer.py b/offer.py index ec21ca3904928ac09b3dfd0a592c6f5431570bb3..1550143cf71196540f1cdd5363b61e2fa5cfe789 100644 --- a/offer.py +++ b/offer.py @@ -1,4 +1,4 @@ import exchange class Offer(exchange.Exchange): - def __init__(self, p: float, ppw: float): - exchange.Exchange.__init__(self, p, ppw) + def __init__(self,pk: bytes, p: float, ppw: float): + exchange.Exchange.__init__(self,pk, p, ppw) diff --git a/participant.py b/participant.py index b841c679afa6f92fb1792e602189607d4dcba43a..33abcd359540cc5eec0142576fc1ac71cb38986d 100644 --- a/participant.py +++ b/participant.py @@ -1,13 +1,13 @@ # from main import ureg, Q_ import locale import time +from datetime import datetime, timedelta from random import randint import ipaddress import asyncio import httpx - import jsonpickle from dilithium import Dilithium @@ -24,10 +24,11 @@ from remoteparticipant import RemoteParticipant from fastapi import APIRouter import logging - logger = logging.getLogger(__name__) import socket + + def get_pub_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(0) @@ -45,8 +46,8 @@ def get_pub_ip(): class Participant: def __init__(self, nid: str, dht_ep: set[ipaddress.IPv4Address], ip: ipaddress = get_pub_ip()): - d1 = Dilithium(dilithium.dilithium.DEFAULT_PARAMETERS["dilithium3"]) - self.__secretKey, self.__publicKey = d1.keygen( + self.dil = Dilithium(dilithium.dilithium.DEFAULT_PARAMETERS["dilithium3"]) + self.__secretKey, self.__publicKey = self.dil.keygen( [randint(0, numpy.iinfo('uint32').max), randint(0, numpy.iinfo('uint32').max), randint(0, numpy.iinfo('uint32').max), randint(0, numpy.iinfo('uint32').max)]) self.fastRouter = APIRouter() @@ -55,10 +56,10 @@ class Participant: self.publicIP: ipaddress.IPv4Address = ipaddress.IPv4Address( ip) # set to current ipv4, confirm before each new round self.knownIPs: set[ipaddress.IPv4Address] = set() - self.knownParticipants: set[RemoteParticipant] = set() + self.remoteParticipants: set[RemoteParticipant] = set() self.availableExchanges: set[ Exchange] = set() # known available exchanges from other participants for next turns - self.nextExchanges: set[Exchange] = set() # own exchanges for next turn + self.nextExchange: Request|Offer # own exchange for next turn, either offering or requesting energy self.activeTrades: set[Trade] = set() # own active trades for this turn self.__tradeHistory: list[Trade] = [] # every own past trade # self.__currentPower = Q_(0, ureg.watt) # real time power exchange with the grid @@ -68,7 +69,8 @@ class Participant: # self.__projectedInhouseSupply = Q_(0, ureg.watt) # expected supply for next round # register rest endpoints with fastapi - self.fastRouter.add_api_route("/get_remoteparticipants_asJSON", self.getremoteParticipants_asJSON, methods=["GET"]) + self.fastRouter.add_api_route("/get_remoteparticipants_asJSON", self.getremoteParticipants_asJSON, + methods=["GET"]) self.fastRouter.add_api_route("/get_knownIPs", self.get_knownIPs, methods=["GET"]) # announce self on dht asyncio.run(self.dht_startup()) @@ -84,7 +86,8 @@ class Participant: logger.info("dht_startup on " + self.publicIP.__str__()) for ip in self.dht_endpoints: # register participant on all dht endpoints async with httpx.AsyncClient() as client: - await client.put("http://" + ip.__str__() + ":8000/addPeerTo/" + self.__dht_network_id + "?ip=" + self.publicIP.__str__()) + await client.put( + "http://" + ip.__str__() + ":8000/addPeerTo/" + self.__dht_network_id + "?ip=" + self.publicIP.__str__()) async def dht_update_peers(self) -> None: for ip in self.dht_endpoints: @@ -97,19 +100,37 @@ class Participant: async def getremoteParticipants_asJSON(self): all_remoteparticipants: set[RemoteParticipant] = set() all_remoteparticipants.add(self.as_remoteParticipant()) - all_remoteparticipants.update(self.knownParticipants) + all_remoteparticipants.update(self.remoteParticipants) return jsonpickle.encode(all_remoteparticipants) + def update_remoteparticipants(self, new_rps: set[RemoteParticipant], target_rps: set[RemoteParticipant]): + for new_rp in new_rps: + for rp in target_rps: + if new_rp.publicKey == rp.publicKey: # found same participant + rp. + + async def requestremoteParticipants(self) -> None: all_remoteparticipants: set[RemoteParticipant] = set() - for ip in self.knownIPs: - url = 'http://' + ip.__str__() + ':8000/get_remoteparticipants_asJSON' - async with httpx.AsyncClient() as client: - response = await client.get(url) - remoteparticipants: set[RemoteParticipant] = jsonpickle.decode(response.json()) - # some validation of remoteparticipants should happen, not in scope of this thesis - all_remoteparticipants.update(remoteparticipants) - self.knownParticipants.update(all_remoteparticipants) + if len(self.remoteParticipants) == 0: # initial discovery via dht + await self.dht_update_peers() + for ip in self.knownIPs: + try: + url = 'http://' + ip.__str__() + ':8000/get_remoteparticipants_asJSON' + async with httpx.AsyncClient() as client: + response = await client.get(url) + remoteparticipants: set[RemoteParticipant] = jsonpickle.decode(response.json()) + # some validation of remoteparticipants should happen, not in scope of this thesis + all_remoteparticipants.update(remoteparticipants) + except httpx.HTTPError as err: + logger.error("httpx Error: " + err.__str__()) + continue + + self.remoteParticipants.update(all_remoteparticipants) + self.knownIPs.clear() + return + else: # continued peer updates without dht, periodic dht discovery should still happen to discover new users + async def get_knownIPs(self): return jsonpickle.encode(self.knownIPs) diff --git a/remoteparticipant.py b/remoteparticipant.py index 98c2e3dd4cf1587bef83ddb5aa0eb681202b351b..2f5441b2e816c048e5881cebd7aea6706155db04 100644 --- a/remoteparticipant.py +++ b/remoteparticipant.py @@ -1,4 +1,6 @@ import ipaddress +import time +from datetime import datetime,timedelta from pydantic.dataclasses import dataclass @@ -6,13 +8,33 @@ import exchange class RemoteParticipant: - def __init__(self, pk: bytes, ip: ipaddress.IPv4Address, nex: set[exchange.Exchange]): - self.__publicKey: bytes = pk + def __init__(self, pk: bytes, ip: ipaddress.IPv4Address, nex: list[exchange.Exchange]): + self.publicKey: bytes = pk self.publicIP: ipaddress.IPv4Address = ip - self.__nextExchanges: set[exchange.Exchange] = nex + self.nextExchanges: list[exchange.Exchange] = nex + self.timestamp = time.time_ns() - def __eq__(self, other): - return self.__publicKey == other.__publicKey + def update_nextExchanges(self, exchanges: list[exchange.Exchange]): + for new_ex in exchanges: + for ex in self.nextExchanges: + # both exchanges origin from same participant and are within the next minute + if new_ex.pubicKey == ex.pubicKey and new_ex.is_min_next() and ex.is_min_next(): + ex. - def __hash__(self): - return super.__hash__(self) \ No newline at end of file + + + def is_min_now(self) -> bool: + now = datetime.now() + + # Calculate the first second of the current minute + start_of_minute = datetime(now.year, now.month, now.day, now.hour, now.minute, 0) + start_of_minute_unix = int(start_of_minute.timestamp()) + + # Calculate the last second of the current minute + end_of_minute = start_of_minute + timedelta(seconds=59) + end_of_minute_unix = int(end_of_minute.timestamp()) + + if start_of_minute_unix <= self.timestamp <= end_of_minute_unix: + return True + else: + return False diff --git a/request.py b/request.py index 5a734dd6e323855f988c504fdfc9382e4f15dd4d..805c613b07ff20a9bb9e5ddc23c59f836e075e01 100644 --- a/request.py +++ b/request.py @@ -1,7 +1,6 @@ import exchange -class Request(exchange.Exchange): - def __init__(self, p: float, ppw: float): - exchange.Exchange.__init__(self, p,ppw) - +class Request(exchange.Exchange): + def __init__(self, pk: bytes, p: float, ppw: float): + exchange.Exchange.__init__(self, pk, p, ppw) diff --git a/trade.py b/trade.py index b28dcd8b13e27bd5257d672efe92d4a224ff6fe2..93e16003837a0184e0cbc06eda63ace62893370e 100644 --- a/trade.py +++ b/trade.py @@ -12,4 +12,5 @@ class Trade: self.__request: request.Request = r #self.__power = Q_(p, ureg.watt) self.__pricePerWatt = locale.currency(ppw) - self.__sig: bytes \ No newline at end of file + self.sig_off: bytes + self.sig_req: bytes \ No newline at end of file