Skip to content

Commit

Permalink
Read Bidirectionality (#87)
Browse files Browse the repository at this point in the history
* Add support for reading bidirectionality from API

* Throw when making a request without properly logging in.

* Bump version to 0.18.6
  • Loading branch information
magico13 authored Jul 30, 2024
1 parent 70979a4 commit fd1ce45
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 76 deletions.
2 changes: 1 addition & 1 deletion pyemvue/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "0.18.5"
VERSION = "0.18.6"
21 changes: 9 additions & 12 deletions pyemvue/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

# These provide AWS cognito authentication support
from pycognito import Cognito
from pycognito.exceptions import TokenVerificationException

CLIENT_ID = "4qte47jbstod8apnfic0bunmrq"
USER_POOL = "us-east-2_ghlOXVLi1"
Expand Down Expand Up @@ -35,6 +34,7 @@ def __init__(
self.initial_retry_delay = max(initial_retry_delay, 0.5)
self.max_retry_delay = max(max_retry_delay, 0)
self.pool_wellknown_jwks = None
self.tokens = {}

self._password = None

Expand Down Expand Up @@ -62,17 +62,11 @@ def __init__(

def refresh_tokens(self) -> "dict[str, str]":
"""Refresh and return new tokens."""
try:
if self._password:
self.cognito.authenticate(password=self._password)

self.cognito.renew_access_token()
except TokenVerificationException as ex:
# ignore iat errors (until https://github.com/NabuCasa/pycognito/issues/225 is fixed)
if "The token is not yet valid (iat)" not in ex.args[0]:
raise
finally:
self._password = None
if self._password:
self.cognito.authenticate(password=self._password)

self.cognito.renew_access_token()
self._password = None

tokens = self._extract_tokens_from_cognito()
self.tokens = tokens
Expand All @@ -89,6 +83,9 @@ def get_username(self) -> str:

def request(self, method: str, path: str, **kwargs) -> requests.Response:
"""Make a request."""
if not self.tokens or not self.tokens["access_token"]:
raise ValueError("Not authenticated. Incorrect username or password?")

dec_access_token = self._decode_token(self.tokens["access_token"])

attempts = 0
Expand Down
8 changes: 8 additions & 0 deletions pyemvue/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def __init__(self, gid=0, manId="", modelNum="", firmwareVersion=""):

# extra info
self.device_name = ""
self.display_name = ""
self.zip_code = "00000"
self.time_zone = ""
self.usage_cent_per_kw_hour = 0.0
Expand Down Expand Up @@ -84,6 +85,8 @@ def populate_location_properties_from_json(self, js: "dict[str, Any]"):
"""Adds the values from the get_device_properties method."""
if "deviceName" in js:
self.device_name = js["deviceName"]
if "displayName" in js:
self.display_name = js["displayName"]
if "zipCode" in js:
self.zip_code = js["zipCode"]
if "timeZone" in js:
Expand Down Expand Up @@ -138,6 +141,7 @@ def __init__(
self.channel_multiplier = channelMultiplier
self.channel_type_gid = channelTypeGid
self.nested_devices = {}
self.type = ""

def from_json_dictionary(self, js: "dict[str, Any]") -> Self:
"""Populate device channel data from a dictionary extracted from the response json."""
Expand All @@ -151,7 +155,11 @@ def from_json_dictionary(self, js: "dict[str, Any]") -> Self:
self.channel_multiplier = js["channelMultiplier"]
if "channelTypeGid" in js:
self.channel_type_gid = js["channelTypeGid"]
if "type" in js:
self.type = js["type"]
return self

# Known types: Main, FiftyAmp, FiftyAmpBidirectional

def as_dictionary(self) -> "dict[str, Any]":
"""Returns a dictionary of the device channel data."""
Expand Down
2 changes: 1 addition & 1 deletion pyemvue/pyemvue.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def get_chart_usage(
) -> "tuple[list[float], Optional[datetime.datetime]]":
"""Gets the usage over a given time period and the start of the measurement period. Note that you may need to scale this to get a rate (1MIN in kw = 60*result)"""
if channel.channel_num in ["MainsFromGrid", "MainsToGrid"]:
# These is not populated for the special Mains data as of right now
# This is not populated for the special Mains data as of right now
return [], start
if not start:
start = datetime.datetime.now(datetime.timezone.utc)
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pycognito>=2024.2.0
pycognito>=2024.5.0
python-dateutil>=2.8.2
requests>=2.26.0
typing_extensions>=4.0.1
136 changes: 81 additions & 55 deletions simulator/channel_types.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,131 +2,157 @@
{
"channelTypeGid": 1,
"description": "Air Conditioner",
"selectable": true
},
{
"channelTypeGid": 2,
"description": "Battery",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 3,
"description": "Boiler",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 4,
"description": "Clothes Dryer",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 5,
"description": "Clothes Washer",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 6,
"description": "Computer/Network",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 7,
"description": "Cooktop/Range/Oven/Stove",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 8,
"description": "Dishwasher",
"selectable": true
},
{
"channelTypeGid": 9,
"description": "Electric Vehicle/RV",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 10,
"description": "Fridge/Freezer",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 11,
"description": "Furnace",
"selectable": true
},
{
"channelTypeGid": 12,
"description": "Garage/Shop/Barn/Shed",
"selectable": true
},
{
"channelTypeGid": 24,
"description": "Heat Pump",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 14,
"description": "Hot Tub/Spa",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 15,
"description": "Humidifier/Dehumidifier",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 16,
"description": "Kitchen",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 17,
"description": "Microwave",
"selectable": true
},
{
"channelTypeGid": 18,
"description": "Other",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 19,
"description": "Pump",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 20,
"description": "Room/Multi-use Circuit",
"selectable": true
},
{
"channelTypeGid": 21,
"description": "Sub Panel",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 22,
"description": "Water Heater",
"selectable": true
},
{
"channelTypeGid": 13,
"description": "Solar/Generation",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 23,
"description": "Smart Outlet",
"selectable": false
"selectable": false,
"allowsBidirectional": false
},
{
"channelTypeGid": 25,
"description": "EV Charger",
"selectable": false
"selectable": false,
"allowsBidirectional": false
},
{
"channelTypeGid": 26,
"description": "Lights",
"selectable": true
"selectable": true,
"allowsBidirectional": false
},
{
"channelTypeGid": 2,
"description": "Battery",
"selectable": true,
"allowsBidirectional": true
},
{
"channelTypeGid": 9,
"description": "Electric Vehicle/RV",
"selectable": true,
"allowsBidirectional": true
},
{
"channelTypeGid": 12,
"description": "Garage/Shop/Barn/Shed",
"selectable": true,
"allowsBidirectional": true
},
{
"channelTypeGid": 24,
"description": "Heat Pump",
"selectable": true,
"allowsBidirectional": true
},
{
"channelTypeGid": 18,
"description": "Other",
"selectable": true,
"allowsBidirectional": true
},
{
"channelTypeGid": 21,
"description": "Sub Panel",
"selectable": true,
"allowsBidirectional": true
},
{
"channelTypeGid": 13,
"description": "Solar/Generation",
"selectable": true,
"allowsBidirectional": true
}
]
]
8 changes: 6 additions & 2 deletions simulator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@
# plug 1 is pulling 5 amps at 120 volts
state.set_channel_1min_watts(1001, "1,2,3", 5 * 120)

# the overall house is pulling 95 amps at 240 volts
state.set_channel_1min_watts(1000, "1,2,3", 95 * 240)
# channel 2 is generating 10 amps and is bidirectional
state.set_channel_bidirectionality(1000, "2", True)
state.set_channel_1min_watts(1000, "2", -10 * 120)

# the overall house is pulling 85 amps at 240 volts
state.set_channel_1min_watts(1000, "1,2,3", 85 * 240)

# the balance is effectively 40 amps
state.set_channel_1min_watts(1000, "Balance", 40 * 240)
Expand Down
3 changes: 3 additions & 0 deletions simulator/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class SimulatorChannel(SimulatorBase):
channelNum: str
channelMultiplier: float = 1.0
channelTypeGid: Optional[int]
type: str


class SimulatorLatitudeLongitude(SimulatorBase):
Expand All @@ -109,6 +110,7 @@ class SimulatorLocationProperties(SimulatorBase):
latitudeLongitude: SimulatorLatitudeLongitude
utilityRateGid: Optional[int]
deviceName: Optional[str]
displayName: Optional[str]
deviceGid: int
zipCode: str
billingCycleStartDay: int
Expand Down Expand Up @@ -154,6 +156,7 @@ class ChannelType(SimulatorBase):
channelTypeGid: int
description: str
selectable: bool
allowsBidirectional: bool


class SimulatorLoad(SimulatorBase):
Expand Down
Loading

0 comments on commit fd1ce45

Please sign in to comment.