Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time Of Use Settings Error #443

Closed
craigrouse opened this issue Dec 4, 2023 · 7 comments
Closed

Time Of Use Settings Error #443

craigrouse opened this issue Dec 4, 2023 · 7 comments

Comments

@craigrouse
Copy link
Contributor

craigrouse commented Dec 4, 2023

Through the Home Assistant integration (https://github.com/alandtse/tesla/), I've been trying to send a custom command to change the time of use settings on the Powerwall - specifically, to send a new tariff with prices and seasons. When I run the command in Postman, it works perfectly, and I get a 201 API response, and see the result reflected in the Tesla app. When I try this through the custom Home Assistant plugin, I get a "400: err Not Supported" error returned, and the change isn't reflected in the Tesla app. I'm using the service call through Developer Tools with the Command field set to "TIME_OF_USE_SETTINGS", path_vars containing my site ID, and the JSON below converted to a YAML dictionary.

I've dug into the underlying code in connection.py, and can't see anything obviously wrong; in theory, the POST request should be formed correctly with the dictionary that's passed in, but whatever I do, I just can't get it working. I've tried to add some extra logging and install this dependency as a custom component, but it doesn't seem to be working - my error, I'm sure.

Note that I am seeing something that looks correct in the logs - the URL correctly includes my site ID, and the payload looks like a valid JSON, albeit with single quotes instead of double quotes, because it's logged before being converted to a real JSON. I'm sure if I could see the full outbound HTTP request, I'd be able to spot the problem right away, but this is tricky to achieve.

In case anyone here can help, here's the request I'm trying to send, which works perfectly every time through Postman:

URL: https://owner-api.teslamotors.com/api/1/energy_sites/<my_site_id>/time_of_use_settings
Headers: X-Tesla-User-Agent = TeslaApp/4.8.0-1025/3e45238df4/android/11
Authorization: Bearer token

{"tou_settings": {
    "tariff_content": {
      "name": "Intelligent",
      "utility": "Octopus",
      "daily_charges": [
        {
          "amount": 0,
          "name": "Charge"
        }
      ],
      "demand_charges": {
        "ALL": {
          "ALL": 0
        },
        "Summer": {
        },
        "Winter": {
        }
      },
      "energy_charges": {
        "ALL": {
          "ALL": 0
        },
        "Summer": {
          "ON_PEAK": 99.99,
          "SUPER_OFF_PEAK": 0.08
        },
        "Winter": {
        }
      },
      "seasons": {
        "Summer": {
          "fromDay": 1,
          "toDay": 31,
          "fromMonth": 1,
          "toMonth": 12,
          "tou_periods": {
            "ON_PEAK": [
              {
                "fromDayOfWeek": 0,
                "toDayOfWeek": 6,
                "fromHour": 5,
                "fromMinute": 30,
                "toHour": 23,
                "toMinute": 30
              }
            ],
            "SUPER_OFF_PEAK": [
              {
                "fromDayOfWeek": 0,
                "toDayOfWeek": 6,
                "fromHour": 23,
                "fromMinute": 30,
                "toHour": 5,
                "toMinute": 30
              }
            ]
          }
        },
        "Winter": {
          "fromDay": 0,
          "toDay": 0,
          "fromMonth": 0,
          "toMonth": 0,
          "tou_periods": {
          }
        }
      },
      "sell_tariff": {
        "name": "Intelligent",
        "utility": "Octopus",
        "daily_charges": [
          {
            "amount": 0,
            "name": "Charge"
          }
        ],
        "demand_charges": {
          "ALL": {
            "ALL": 0
          },
          "Summer": {
          },
          "Winter": {
          }
        },
        "energy_charges": {
          "ALL": {
            "ALL": 0
          },
          "Summer": {
            "ON_PEAK":0.15,
            "SUPER_OFF_PEAK": 0.05
          },
          "Winter": {
          }
        },
        "seasons": {
          "Summer": {
            "fromDay": 1,
            "toDay": 31,
            "fromMonth": 1,
            "toMonth": 12,
            "tou_periods": {
              "ON_PEAK": [
                {
                  "fromDayOfWeek": 0,
                  "toDayOfWeek": 6,
                  "fromHour": 5,
                  "fromMinute": 30,
                  "toHour": 23,
                  "toMinute": 30
                }
              ],
              "SUPER_OFF_PEAK": [
                {
                  "fromDayOfWeek": 0,
                  "toDayOfWeek": 6,
                  "fromHour": 23,
                  "fromMinute": 30,
                  "toHour": 5,
                  "toMinute": 30
                }
              ]
            }
          },
          "Winter": {
            "fromDay": 0,
            "toDay": 0,
            "fromMonth": 0,
            "toMonth": 0,
            "tou_periods": {
            }
          }
        }
      }
    }
  }
}

Any suggestions are welcome.

@alandtse
Copy link
Collaborator

alandtse commented Dec 4, 2023

You may have to do packet sniffing. But there is a history of Tesla's akamai servers detecting non app connections from the fingerprint and denying access. We swapped from aiohttp to httpx because of that.

@craigrouse
Copy link
Contributor Author

@alandtse I don't think that's the case here. I missed one detail - if I pass tou_settings as an empty dictionary, I do actually get a successful response, and it deletes all my time of use settings in the Tesla app. Logically, that points to my dictionary being malformed, but from the logs, it looks perfect, and all I've done is taken the working JSON above and converted it to YAML.

@purcell-lab
Copy link

purcell-lab commented Dec 5, 2023

I have a working solution to change the TOU settings from Home Assistant, inspired by this script from Rob:
https://github.com/rob0101/powerwall_sell_tariff

I am using Home Assistant to call a python shell command which then updates the TOU settings, although your proposed solution to use the TIME_OF_USE endpoints is more elegant and worth debugging, I would suggest it is a formatting of your payload which is causing this issue.

My use case is to move the ON_PEAK window so it is always current, this allows me to discharge the Powerwall on demand by switching to Time Based Control mode (again using Home Assistant). As an aside I am able to charge on demand from Home Assistant by using a mix of automation commands; set Reserve > SOC (1.7 kW), set Backup Mode (3.3 kW), set TBC Mode & period = OFF_PEAK (5 kW).

image

As you can see it isn't foolproof, as sometimes the settings revert, so I have added some additional error handling, to try and catch when it reverts, but even that isn't always functional.

To change TBC settings, I run an automation every hour to set ON_PEAK window to be the last 30 minutes + next 60 minutes and OFF_PEAK at other times. I only want a short 60 minute window so it actually discharges, I find if I make the window much longer, it doesn't discharge straight away:

automation

alias: PowerwallPeak60
description: ""
trigger:
  - platform: time_pattern
    minutes: "40"
  - platform: state
    entity_id:
      - sensor.powerwall_tou_period
    to: OFF_PEAK
    for:
      hours: 0
      minutes: 0
      seconds: 0
    id: OFF_PEAK
condition: []
action:
  - service: shell_command.powerwall_peak60
    data: {}
  - condition: trigger
    id:
      - OFF_PEAK
  - service: notify.mobile_app_pixel_6
    data:
      title: OFF_PEAK
  - delay:
      hours: 0
      minutes: 0
      seconds: 10
      milliseconds: 0
  - service: shell_command.powerwall_peak60
    data: {}
mode: single

config.yaml

shell_command:
  powerwall_peak60: "python3 /config/scripts/amber-tariff.py"

amber-tariff.py

#!/usr/bin/env python3

import json
import sys
import datetime
import teslapy

from teslapy import Tesla, Battery

def set_peak_for_next_60_minutes():
    # Get current time
    now = datetime.datetime.now()

    # Calculate time 60 minutes from now
    end_time = now + datetime.timedelta(minutes=60)
    start_time = now - datetime.timedelta(minutes=30)

    # Format times
    start_hour = start_time.strftime("%H")
    start_minute = start_time.strftime("%M")
    end_hour = end_time.strftime("%H")
    end_minute = end_time.strftime("%M")

    # Create a schedule dictionary
    schedule_dict = {
        "OFF_PEAK": [
            {
                "fromDayOfWeek": 0,
                "toDayOfWeek": 6,
                "fromHour": int(end_hour),
                "fromMinute": int(end_minute),
                "toHour": int(start_hour),
                "toMinute": int(start_minute)
            }
        ],
        "ON_PEAK": [
            {
                "fromDayOfWeek": 0,
                "toDayOfWeek": 6,
                "fromHour": int(start_hour),
                "fromMinute": int(start_minute),
                "toHour": int(end_hour),
                "toMinute": int(end_minute)
            }
        ]
    }

    # Convert the dictionary to a JSON string
    return schedule_dict

# Set the peak for the next 60 minutes
schedule = set_peak_for_next_60_minutes()

# Debugging: Print the schedule
print("Schedule:", schedule)



with teslapy.Tesla('mark@purcell.id.au') as tesla:
    batteries = tesla.battery_list()
    battery = batteries[0]

    tariff = battery.get_tariff()

    tariff["energy_charges"]["Summer"]["ON_PEAK"] = 0.58
    tariff["energy_charges"]["Summer"]["PARTIAL_PEAK"] = 0.28
    tariff["energy_charges"]["Summer"]["OFF_PEAK"] = 0.16
    tariff["energy_charges"]["Summer"]["SUPER_OFF_PEAK"] = 0.05

    tariff["sell_tariff"]["energy_charges"]["Summer"]["ON_PEAK"] = 0.37
    tariff["sell_tariff"]["energy_charges"]["Summer"]["PARTIAL_PEAK"] = 0.05
    tariff["sell_tariff"]["energy_charges"]["Summer"]["OFF_PEAK"] = 0
    tariff["sell_tariff"]["energy_charges"]["Summer"]["SUPER_OFF_PEAK"] = 0

    battery.set_tariff(tariff)

    # Debugging: Print the original tariff
    #print("Original Tariff:", tariff)

    tariff["seasons"]["Summer"]["tou_periods"] = schedule
    tariff["sell_tariff"]["seasons"]["Summer"]["tou_periods"] = schedule

    # Debugging: Print the updated tariff
    #print("Updated Tariff:", tariff)

    tariff["name"] = "Custom via API"
    tariff["utility"] = "Amber"

    battery.set_tariff(tariff)

@craigrouse
Copy link
Contributor Author

craigrouse commented Dec 5, 2023

Thanks for your suggestions @purcell-lab. I was also considering a custom python script, but it should work using a custom API command, and would be so much easier. It definitely feels like a formatting issue, especially since I can get it to respond successfully, albeit with an empty payload that deletes my ToU settings. Perhaps you can spot something wrong with my formatting here?

service: tesla_custom.api
data:
  email: <my_email>
  command: TIME_OF_USE_SETTINGS
  parameters:
    path_vars:
      site_id: "<my_site_id>"
    tou_settings:
      tariff_content:
        name: Intelligent HA
        utility: Octopus
        daily_charges:
          - amount: 0
            name: Charge
        demand_charges:
          ALL:
            ALL: 0
          Summer: {}
          Winter: {}
        energy_charges:
          ALL:
            ALL: 0
          Summer:
            ON_PEAK: 99.99
            SUPER_OFF_PEAK: 0.08
          Winter: {}
        seasons:
          Summer:
            fromDay: 1
            toDay: 31
            fromMonth: 1
            toMonth: 12
            tou_periods:
              ON_PEAK:
                - fromDayOfWeek: 0
                  toDayOfWeek: 6
                  fromHour: 5
                  fromMinute: 30
                  toHour: 23
                  toMinute: 30
              SUPER_OFF_PEAK:
                - fromDayOfWeek: 0
                  toDayOfWeek: 6
                  fromHour: 23
                  fromMinute: 30
                  toHour: 5
                  toMinute: 30
          Winter:
            fromDay: 0
            toDay: 0
            fromMonth: 0
            toMonth: 0
            tou_periods: {}
        sell_tariff:
          name: Intelligent HA
          utility: Octopus
          daily_charges:
            - amount: 0
              name: Charge
          demand_charges:
            ALL:
              ALL: 0
            Summer: {}
            Winter: {}
          energy_charges:
            ALL:
              ALL: 0
            Summer:
              ON_PEAK: 0.15
              SUPER_OFF_PEAK: 0.05
            Winter: {}
          seasons:
            Summer:
              fromDay: 1
              toDay: 31
              fromMonth: 1
              toMonth: 12
              tou_periods:
                ON_PEAK:
                  - fromDayOfWeek: 0
                    toDayOfWeek: 6
                    fromHour: 5
                    fromMinute: 30
                    toHour: 23
                    toMinute: 30
                SUPER_OFF_PEAK:
                  - fromDayOfWeek: 0
                    toDayOfWeek: 6
                    fromHour: 23
                    fromMinute: 30
                    toHour: 5
                    toMinute: 30
            Winter:
              fromDay: 0
              toDay: 0
              fromMonth: 0
              toMonth: 0
              tou_periods: {}

If I send this:

path_vars:
  site_id: "<my_site_id>"
tou_settings:
  tariff_content:

... I get a 201 response, and it deletes my ToU settings, so this is how I know it's not totally broken. It feels to me like there's something strange going on when the YAML is converted to JSON for the final payload, but without being able to see the actual HTTP request, it's hard to confirm this.

@alandtse I'm not sure how to go about packet sniffing to view the real payload - the traffic is obviously encrypted, and it's not possible to set a proxy easily in HA. Let me know if you have any suggestions, and I'll give them a try.

@alandtse
Copy link
Collaborator

alandtse commented Dec 5, 2023

https://mitmproxy.org/

@craigrouse
Copy link
Contributor Author

@alandtse I spent a bit of time hacking, and created a 2nd api method to override the base url to send my data to requestbin. I was then able to identify that the missing param that was causing the failure was the header x-tesla-user-agent. This can be passed with the same value as user_agent - it just needs to be present, and the endpoint will then accept it. I've now modified the relevant section in connection.py in the sethead method, and I can now successfully call the Time Of Use settings API from the API service in the HA Tesla Custom integration. I'll gladly submit a PR to add this extra header if that's the best way forward? @purcell-lab I think you'll be happy to hear this! It's a very simple update, and means you don't need an extra python script 🎉

@alandtse
Copy link
Collaborator

alandtse commented Dec 7, 2023

Glad you figured it out. Yes, just submit a PR and tag me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants