diff --git a/README.md b/README.md index e789707..2b3c7fb 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,10 @@ and call function to poll data. Here is an example: set_mode(mode) # Set Current Battery Operation Mode get_time_remaining() # Get the backup time remaining on the battery set_operation(level, mode, json) # Set Battery Reserve Percentage and/or Operation Mode + set_grid_charging(mode) # Enable or disable grid charging (mode = True or False) + set_grid_export(mode) # Set grid export mode (mode = battery_ok, pv_only, never) + get_grid_charging() # Get the current grid charging mode + get_grid_export() # Get the current grid export mode ``` ## Tools @@ -243,16 +247,18 @@ Options: Commands (run -h to see usage information): {setup,scan,set,get,version} - setup Setup Tesla Login for Cloud Mode access - scan Scan local network for Powerwall gateway - set Set Powerwall Mode and Reserve Level - get Get Powerwall Settings and Power Levels - version Print version information + setup Setup Tesla Login for Cloud Mode access + scan Scan local network for Powerwall gateway + set Set Powerwall Mode and Reserve Level + get Get Powerwall Settings and Power Levels + version Print version information set options: - -mode MODE Powerwall Mode: self_consumption, backup, or autonomous - -reserve RESERVE Set Battery Reserve Level [Default=20] - -current Set Battery Reserve Level to Current Charge + -mode MODE Powerwall Mode: self_consumption, backup, or autonomous + -reserve RESERVE Set Battery Reserve Level [Default=20] + -current Set Battery Reserve Level to Current Charge + -gridcharging MODE Set Grid Charging (allow) Mode ("on" or "off") + -gridexport MODE Set Export to Grid Mode ("battery_ok", "pv_only", or "never") get options: -format FORMAT Output format: text, json, csv diff --git a/RELEASE.md b/RELEASE.md index 150ebfe..cf5c4a4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,54 @@ # RELEASE NOTES +## v0.10.10 - Add Grid Control + +* Add a function and command line options to allow user to get and set grid charging and exporting modes (see https://github.com/jasonacox/pypowerwall/issues/108). +* Supports FleetAPI and Cloud modes only (not Local mode) + +#### Command Line Examples + +```bash +# Connect to Cloud +python3 -m pypowerwall setup # or fleetapi + +# Get Current Settings +python3 -m pypowerwall get + +# Turn on Grid charging +python3 -m pypowerwall set -gridcharging on + +# Turn off Grid charging +python3 -m pypowerwall set -gridcharging off + +# Set Grid Export to Solar (PV) energy only +python3 -m pypowerwall set -gridexport pv_only + +# Set Grid Export to Battery and Solar energy +python3 -m pypowerwall set -gridexport battery_ok + +# Disable export of all energy to grid +python3 -m pypowerwall set -gridexport never +``` + +#### Programming Examples + +```python +import pypowerwall + +# FleetAPI Mode +PW_HOST="" +PW_EMAIL="my@example.com" +pw = pypowerwall.Powerwall(host=PW_HOST, email=PW_EMAIL, fleetapi=True) + +# Get modes +pw.get_grid_charging() +pw.get_grid_export() + +# Set modes +pw.set_grid_charging("on") # set grid charging mode (on or off) +pw.set_grid_export("pv_only") # set grid export mode (battery_ok, pv_only, or never) +``` + ## v0.10.9 - TEDAPI Voltage & Current * Add computed voltage and current to `/api/meters/aggregates` from TEDAPI status data. diff --git a/pypowerwall/__init__.py b/pypowerwall/__init__.py index 949bc7a..d44ca60 100644 --- a/pypowerwall/__init__.py +++ b/pypowerwall/__init__.py @@ -71,6 +71,10 @@ set_mode(mode) # Set Current Battery Operation Mode get_time_remaining() # Get the backup time remaining on the battery set_operation(level, mode, json) # Set Battery Reserve Percentage and/or Operation Mode + set_grid_charging(mode) # Enable or disable grid charging (mode = True or False) + set_grid_export(mode) # Set grid export mode (mode = battery_ok, pv_only, never) + get_grid_charging() # Get the current grid charging mode + get_grid_export() # Get the current grid export mode Requirements This module requires the following modules: requests, protobuf, teslapy @@ -84,7 +88,7 @@ from typing import Union, Optional import time -version_tuple = (0, 10, 9) +version_tuple = (0, 10, 10) version = __version__ = '%d.%d.%d' % version_tuple __author__ = 'jasonacox' @@ -835,6 +839,51 @@ def get_time_remaining(self) -> Optional[float]: """ return self.client.get_time_remaining() + def set_grid_charging(self, mode) -> Optional[dict]: + """ + Enable or disable grid charging + + Args: + mode: Set to True to enable grid charging, False to disable it + + Returns: + Dictionary with operation results. + """ + return self.client.set_grid_charging(mode) + + def get_grid_charging(self) -> Optional[bool]: + """ + Get the current grid charging mode + + Returns: + True if grid charging is enabled, False if it is disabled + """ + return self.client.get_grid_charging() + + def set_grid_export(self, mode: str) -> Optional[dict]: + """ + Set grid export mode + + Args: + mode: Set grid export mode (battery_ok, pv_only, or never) + + Returns: + Dictionary with operation results. + """ + # Check for valid mode + if mode not in ['battery_ok', 'pv_only', 'never']: + raise ValueError(f"Invalid value for parameter 'mode': {mode}") + return self.client.set_grid_export(mode) + + def get_grid_export(self) -> Optional[str]: + """ + Get the current grid export mode + + Returns: + The current grid export mode + """ + return self.client.get_grid_export() + def _validate_init_configuration(self): # Basic user input validation for starters. Can be expanded to limit other parameters such as cache diff --git a/pypowerwall/__main__.py b/pypowerwall/__main__.py index 49fa871..b58e16f 100644 --- a/pypowerwall/__main__.py +++ b/pypowerwall/__main__.py @@ -57,6 +57,10 @@ help="Set Battery Reserve Level [Default=20]") set_mode_args.add_argument("-current", action="store_true", default=False, help="Set Battery Reserve Level to Current Charge") +set_mode_args.add_argument("-gridcharging", type=str, default=None, + help="Enable Grid Charging Mode: on or off") +set_mode_args.add_argument("-gridexport", type=str, default=None, + help="Grid Export Mode: battery_ok, pv_only, or never") get_mode_args = subparsers.add_parser("get", help='Get Powerwall Settings and Power Levels') get_mode_args.add_argument("-format", type=str, default="text", @@ -129,8 +133,8 @@ # Set Powerwall Mode elif command == 'set': # If no arguments, print usage - if not args.mode and not args.reserve and not args.current: - print("usage: pypowerwall set [-h] [-mode MODE] [-reserve RESERVE] [-current]") + if not args.mode and not args.reserve and not args.current and not args.gridcharging and not args.gridexport: + print("usage: pypowerwall set [-h] [-mode MODE] [-reserve RESERVE] [-current] [-gridcharging MODE] [-gridexport MODE]") sys.exit(1) import pypowerwall # Determine which cloud mode to use @@ -154,6 +158,20 @@ current = float(pw.level()) print("Setting Powerwall Reserve to Current Charge Level %s" % current) pw.set_reserve(current) + if args.gridcharging: + gridcharging = args.gridcharging.lower() + if gridcharging not in ['on', 'off']: + print("ERROR: Invalid Grid Charging Mode [%s] - must be on or off" % gridcharging) + sys.exit(1) + print("Setting Grid Charging Mode to %s" % gridcharging) + pw.set_grid_charging(gridcharging) + if args.gridexport: + gridexport = args.gridexport.lower() + if gridexport not in ['battery_ok', 'pv_only', 'never']: + print("ERROR: Invalid Grid Export Mode [%s] - must be battery_ok, pv_only, or never" % gridexport) + sys.exit(1) + print("Setting Grid Export Mode to %s" % gridexport) + pw.set_grid_export(gridexport) # Get Powerwall Mode elif command == 'get': @@ -177,6 +195,8 @@ 'home': pw.home(), 'battery': pw.battery(), 'solar': pw.solar(), + 'grid_charging': pw.get_grid_charging(), + 'grid_export_mode': pw.get_grid_export(), } if args.format == 'json': print(json.dumps(output, indent=2)) @@ -190,7 +210,8 @@ # Table Output for item in output: name = item.replace("_", " ").title() - print(" {:<15}{}".format(name, output[item])) + print(" {:<18}{}".format(name, output[item])) + print("") # Print Version elif command == 'version': diff --git a/pypowerwall/cloud/pypowerwall_cloud.py b/pypowerwall/cloud/pypowerwall_cloud.py index aef070b..2da9d39 100644 --- a/pypowerwall/cloud/pypowerwall_cloud.py +++ b/pypowerwall/cloud/pypowerwall_cloud.py @@ -469,7 +469,6 @@ def get_site_config(self, force: bool = False): "vpp_backup_reserve_percent": 80 } } - """ # GET api/1/energy_sites/{site_id}/site_info (response, _) = self._site_api("SITE_CONFIG", SITE_CONFIG_TTL, language="en", force=force) @@ -756,6 +755,63 @@ def get_api_system_status(self, **kwargs) -> Optional[Union[dict, list, str, byt return data + def set_grid_charging(self, mode: str) -> bool: + """ + Enable/Disable grid charging mode (mode: "on" or "off") + """ + if mode in ["on", "yes"] or mode is True: + mode = False + elif mode in ["off", "no"] or mode is False: + mode = True + else: + log.debug(f"Invalid mode: {mode}") + return False + response = self._site_api("ENERGY_SITE_IMPORT_EXPORT_CONFIG", ttl=SITE_CONFIG_TTL, force=True, + disallow_charge_from_grid_with_solar_installed = mode) + # invalidate cache + super()._invalidate_cache("SITE_CONFIG") + self.pwcachetime["SITE_CONFIG"] = 0 + return response + + def set_grid_export(self, mode: str) -> bool: + """ + Set grid export mode (battery_ok, pv_only, or never) + + Mode will show up in get_site_info() under components: + * never + "non_export_configured": true, + "customer_preferred_export_rule": "never", + * pv_only + "customer_preferred_export_rule": "pv_only" + * battery_ok + "customer_preferred_export_rule": "battery_ok" + or not set + """ + if mode not in ["battery_ok", "pv_only", "never"]: + log.debug(f"Invalid mode: {mode} - must be battery_ok, pv_only, or never") + # POST api/1/energy_sites/{site_id}/grid_import_export + response = self._site_api("ENERGY_SITE_IMPORT_EXPORT_CONFIG", ttl=SITE_CONFIG_TTL, force=True, + customer_preferred_export_rule = mode) + # invalidate cache + super()._invalidate_cache("SITE_CONFIG") + self.pwcachetime["SITE_CONFIG"] = 0 + return response + + def get_grid_charging(self, force=False): + """ Get allow grid charging allowed mode (True or False) """ + components = self.get_site_config(force=force).get("response").get("components") or {} + state = components.get("disallow_charge_from_grid_with_solar_installed") + return not state + + def get_grid_export(self, force=False): + """ Get grid export mode (battery_ok, pv_only, or never) """ + components = self.get_site_config(force=force).get("response").get("components") or {} + # Check to see if non_export_configured - pre-PTO setting + if components.get("non_export_configured"): + return "never" + mode = components.get("customer_preferred_export_rule") or "battery_ok" + return mode + # noinspection PyUnusedLocal @not_implemented_mock_data def api_logout(self, **kwargs) -> Optional[Union[dict, list, str, bytes]]: diff --git a/pypowerwall/fleetapi/fleetapi.py b/pypowerwall/fleetapi/fleetapi.py index 36fc674..c1f68e5 100644 --- a/pypowerwall/fleetapi/fleetapi.py +++ b/pypowerwall/fleetapi/fleetapi.py @@ -21,6 +21,8 @@ get_operating_mode() - get operating mode get_history() - get energy history get_calendar_history() - get calendar history + get_grid_charging() - get allow grid charging mode + get_grid_export() - get grid export mode solar_power() - get solar power grid_power() - get grid power battery_power() - get battery power @@ -34,6 +36,8 @@ ... Set set_battery_reserve(reserve) - set battery reserve level (percent) set_operating_mode(mode) - set operating mode (self_consumption or autonomous) + set_grid_charging(mode) - set grid charging mode (on or off) + set_grid_export(mode) - set grid export mode (battery_ok, pv_only, or never) Author: Jason A. Cox Date: 18 Feb 2024 @@ -496,7 +500,23 @@ def get_history(self, kind=None, duration=None, time_zone=None, h = self.poll(f"api/1/energy_sites/{self.site_id}/{history}?{arg_kind}{arg_duration}{arg_time_zone}{arg_start}{arg_end}") return self.keyval(h, "response") + def get_grid_charging(self, force=False): + """ Get allow grid charging allowed mode (True or False) """ + components = self.get_site_info(force=force).get("components") or {} + state = self.keyval(components, "disallow_charge_from_grid_with_solar_installed") or False + return not state + + def get_grid_export(self, force=False): + """ Get grid export mode (battery_ok, pv_only, or never) """ + components = self.get_site_info(force=force).get("components") or {} + # Check to see if non_export_configured - pre-PTO setting + if self.keyval(components, "non_export_configured"): + return "never" + mode = self.keyval(components, "customer_preferred_export_rule") or "battery_ok" + return mode + def set_battery_reserve(self, reserve: int): + """ Set battery reserve level (percent) """ if reserve < 0 or reserve > 100: log.debug(f"Invalid reserve level: {reserve}") return False @@ -508,6 +528,7 @@ def set_battery_reserve(self, reserve: int): return payload def set_operating_mode(self, mode: str): + """ Set operating mode (self_consumption or autonomous) """ data = {"default_real_mode": mode} if mode not in ["self_consumption", "autonomous"]: log.debug(f"Invalid mode: {mode}") @@ -518,6 +539,52 @@ def set_operating_mode(self, mode: str): self.pwcachetime.pop(f"api/1/energy_sites/{self.site_id}/site_info", None) return payload + def set_grid_charging(self, mode: str): + """ Set allow grid charging mode (True or False) + + Mode will show up in get_site_info() under components: + * False + "disallow_charge_from_grid_with_solar_installed": true, + * True + No entry + """ + if mode in ["on", "yes"] or mode is True: + mode = False + elif mode in ["off", "no"] or mode is False: + mode = True + else: + log.debug(f"Invalid mode: {mode}") + return False + data = {"disallow_charge_from_grid_with_solar_installed": mode} + # 'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/energy_sites/{energy_site_id}/grid_import_export' + payload = self.poll(f"api/1/energy_sites/{self.site_id}/grid_import_export", "POST", data) + # Invalidate cache + self.pwcachetime.pop(f"api/1/energy_sites/{self.site_id}/site_info", None) + return payload + + def set_grid_export(self, mode: str): + """ Set grid export mode (battery_ok, pv_only, or never) + + Mode will show up in get_site_info() under components: + * never + "non_export_configured": true, + "customer_preferred_export_rule": "never", + * pv_only + "customer_preferred_export_rule": "pv_only" + * battery_ok + "customer_preferred_export_rule": "battery_ok" + or not set + """ + if mode not in ["battery_ok", "pv_only", "never"]: + log.debug(f"Invalid mode: {mode} - must be battery_ok, pv_only, or never") + return False + data = {"customer_preferred_export_rule": mode} + # 'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/energy_sites/{energy_site_id}/grid_import_export' + payload = self.poll(f"api/1/energy_sites/{self.site_id}/grid_import_export", "POST", data) + # Invalidate cache + self.pwcachetime.pop(f"api/1/energy_sites/{self.site_id}/site_info", None) + return payload + def get_operating_mode(self, force=False): return self.keyval(self.get_site_info(force=force), "default_real_mode") diff --git a/pypowerwall/fleetapi/pypowerwall_fleetapi.py b/pypowerwall/fleetapi/pypowerwall_fleetapi.py index bef722a..091cab3 100644 --- a/pypowerwall/fleetapi/pypowerwall_fleetapi.py +++ b/pypowerwall/fleetapi/pypowerwall_fleetapi.py @@ -777,6 +777,17 @@ def post_api_operation(self, **kwargs): } return resp + def set_grid_charging(self, mode) -> bool: + return self.fleet.set_grid_charging(mode) + + def set_grid_export(self, mode:str) -> bool: + return self.fleet.set_grid_export(mode) + + def get_grid_export(self, force=False) -> str: + return self.fleet.get_grid_export(force=force) + + def get_grid_charging(self, force=False) -> bool: + return self.fleet.get_grid_charging(force=force) if __name__ == "__main__": import sys diff --git a/pypowerwall/local/pypowerwall_local.py b/pypowerwall/local/pypowerwall_local.py index 2027d0c..9ff3de8 100644 --- a/pypowerwall/local/pypowerwall_local.py +++ b/pypowerwall/local/pypowerwall_local.py @@ -452,3 +452,17 @@ def get_time_remaining(self) -> Optional[float]: return d['nominal_energy_remaining'] / load # Default return None + + # Functions not available in local mode + + def set_grid_charging(self, mode: str) -> None: + log.error('Function set_grid_charging not available in local mode') + + def set_grid_export(self, mode: str) -> None: + log.error('Function set_grid_export not available in local mode') + + def get_grid_charging(self, force=False) -> None: + log.error('Function get_grid_charging not available in local mode') + + def get_grid_export(self, force=False) -> None: + log.error('Function get_grid_export not available in local mode')