Skip to content

Instantly share code, notes, and snippets.

@jasonacox
Last active June 14, 2025 19:44
Show Gist options
  • Save jasonacox/91479957d0605248d7eadb919585616c to your computer and use it in GitHub Desktop.
Save jasonacox/91479957d0605248d7eadb919585616c to your computer and use it in GitHub Desktop.
Set up RaspberryPi as Network Router to Powerwall Gateway

RaspberryPi Bridge - Powerwall Router

This will set up a Raspberry Pi to connect to a Tesla Powerwall Gateway (TEG) and bridge that connection to the ethernet connected LAN.

UPDATE for Powerwall Firmware 25.10.1+

As of Tesla Powerwall Firmware 25.10.1, a local host based static route using the Powerwall LAN IP (ie. sudo ip route add 192.168.91.1 via $POWERWALL_IP) is no longer supported by the Powerwall. Tesla is blocking access via this method. You will need to use a bridge method (as shown below) or have your host direclty connect to the WiFi Access Point on your Powerwall (gateway) to get the extended metrics (vitals).

Below is a method to set up a Raspberry Pi to be a bridge between your LAN and the Powerwall, which will require you to set up a local host baed route (ie. sudo ip route add 192.168.91.1 via $RPI_IP). Alternatively, you can see a method to host pypowerwall on Raspberry Pi that connects to both your LAN and Powerwall. That is described here: jasonacox/Powerwall-Dashboard#607

Network Configuration

 ___________________          __________________________           _______________
[ Powerwall Gateway ]        [  Raspberry Pi (Bridge)   ]         [      Host     ]
[       TEG         ]  WiFi  [__________________________]   LAN   [ Linux/Mac/Win ]
[   WiFi: TEG-xxx   ] <----  [ 192.168.91.x | 10.0.1.55 ] <-----> [   10.0.1.65   ]
[   192.168.91.1    ]        [  WiFi (dhcp) |  Ethernet ]         [      LAN      ]
 ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾         [‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾]          ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
                             [   NAT to 192.168.91.x.   ]
                              ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
 

Raspberry Pi

  1. Create or edit /etc/wpa_supplicant/wpa_supplicant.conf:
network={
        ssid="TEG-xxx"
        psk="password"
}
  1. Restart Networking and Test
sudo systemctl restart networking

# Test
ifconfig wlan0
ping -c 1 192.168.91.1
  1. Set up IPv4 Routing and Reboot
# Add IP Forwarding - Uncomment net.ipv4.ip_forward=1
sudo sed -i -e '/^#net\.ipv4\.ip_forward=1/s/^#//' /etc/sysctl.conf
sudo sysctl -w net.ipv4.ip_forward=1

# Restart
sudo reboot
  1. Set up NAT
# The Powerwall will reject IP addresses not in the 192.168.91.x range
# so you will need to set up network address translation.

sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE

# There are various ways to make them persistent, here is one.
sudo apt install iptables-persistent
sudo netfilter-persistent save

Host

On the host, you need to add a route to use the Raspberry Pi as a gateway to get to the Powerwall Gateway.

# Linux
sudo ip r add 192.168.91.0/24 via 10.0.1.55

# MacOS
sudo route add -host 192.168.91.1 10.0.1.55

# Test
ping -c 1 192.168.91.1
curl -ik https://192.168.91.1
@Nexarian
Copy link

Nexarian commented Dec 25, 2024

Any idea how to get this to work for TWO inverters? I've tried many things with NAT, but no luck.

Obviously, the dumb answer is:

user@raspberrypi:~ $ sudo ip route add 192.168.91.0/24 via 192.168.1.250 dev eth0
user@raspberrypi:~ $ sudo ip route add 192.168.91.0/24 via 192.168.1.67 dev eth0
RTNETLINK answers: File exists

This is the script I'm attempting:

user@raspberrypi:~ $ ./ip-routing.sh 
++ INVERTER1_VIRTUAL_IP=192.168.92.100
++ INVERTER2_VIRTUAL_IP=192.168.92.101
++ INVERTER1_REAL_IP=192.168.91.1
++ INVERTER2_REAL_IP=192.168.91.1
++ INVERTER1_GATEWAY=192.168.1.250
++ INVERTER2_GATEWAY=192.168.1.67
+++ ip route
+++ grep default
+++ awk '{print $5}'
++ INTERFACE=eth0
++ TABLE1=inverter1
++ TABLE2=inverter2
++ TABLE1_NUM=101
++ TABLE2_NUM=102
++ sudo iptables -t nat -F OUTPUT
++ sudo iptables -t nat -F PREROUTING
++ sudo iptables -t nat -F POSTROUTING
++ sudo iptables -t mangle -F OUTPUT
++ sudo iptables -t mangle -F PREROUTING
++ sudo iptables -t mangle -F POSTROUTING
++ echo 'Removing existing static routes and policy rules...'
Removing existing static routes and policy rules...
++ sudo ip route flush table 101
Error: ipv4: FIB table does not exist.
Flush terminated
++ true
++ sudo ip route flush table 102
Error: ipv4: FIB table does not exist.
Flush terminated
++ true
++ sudo ip rule del fwmark 101 lookup 101
++ true
++ sudo ip rule del fwmark 102 lookup 102
++ true
++ sudo ip rule del from 192.168.92.100 lookup inverter1
++ echo 'No policy rule for inverter1.'
No policy rule for inverter1.
++ sudo ip rule del from 192.168.92.101 lookup inverter2
++ echo 'No policy rule for inverter2.'
No policy rule for inverter2.
++ sudo ip addr del 192.168.92.100/32 dev eth0
Error: ipv4: Address not found.
++ true
++ sudo ip addr del 192.168.92.101/32 dev eth0
Error: ipv4: Address not found.
++ true
++ sudo sed -i /inverter1/d /etc/iproute2/rt_tables
++ sudo sed -i /inverter2/d /etc/iproute2/rt_tables
++ sudo ip addr add 192.168.92.100/32 dev eth0
++ sudo ip addr add 192.168.92.101/32 dev eth0
++ echo 'Configuring custom routing tables...'
Configuring custom routing tables...
++ echo '101 inverter1'
++ sudo tee -a /etc/iproute2/rt_tables
101 inverter1
++ echo '102 inverter2'
++ sudo tee -a /etc/iproute2/rt_tables
102 inverter2
++ echo 'Adding static routes for each inverter...'
Adding static routes for each inverter...
++ sudo ip route add 192.168.91.0/24 via 192.168.1.250 dev eth0 onlink table 101
++ sudo ip route add 192.168.91.0/24 via 192.168.1.67 dev eth0 onlink table 102
++ echo 'Setting up policy routing...'
Setting up policy routing...
++ sudo ip rule add from 192.168.92.100 lookup 101
++ sudo ip rule add from 192.168.92.101 lookup 102
++ echo 'Setting up iptables packet marking rules...'
Setting up iptables packet marking rules...
++ sudo iptables -t mangle -A PREROUTING -d 192.168.92.100 -j MARK --set-mark 101
++ sudo iptables -t mangle -A PREROUTING -d 192.168.92.101 -j MARK --set-mark 102
++ sudo iptables -t mangle -A OUTPUT -d 192.168.92.100 -j MARK --set-mark 101
++ sudo iptables -t mangle -A OUTPUT -d 192.168.92.101 -j MARK --set-mark 102
++ echo 'Setting up policy routing rules...'
Setting up policy routing rules...
++ sudo ip rule add fwmark 101 lookup 101
++ sudo ip rule add fwmark 102 lookup 102
++ echo 'Setting up DNAT rules...'
Setting up DNAT rules...
++ sudo iptables -t nat -A OUTPUT -m mark --mark 101 -d 192.168.92.100 -p tcp --dport 443 -j DNAT --to-destination 192.168.91.1
++ sudo iptables -t nat -A OUTPUT -m mark --mark 102 -d 192.168.92.101 -p tcp --dport 443 -j DNAT --to-destination 192.168.91.1
++ sudo iptables -t nat -A OUTPUT -m mark --mark 101 -d 192.168.92.100 -p tcp --dport 80 -j DNAT --to-destination 192.168.91.1
++ sudo iptables -t nat -A OUTPUT -m mark --mark 102 -d 192.168.92.101 -p tcp --dport 80 -j DNAT --to-destination 192.168.91.1
++ echo 'Setting up SNAT rules...'
Setting up SNAT rules...
++ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
++ sudo sysctl -w net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.all.rp_filter = 0
++ sudo sysctl -w net.ipv4.conf.default.rp_filter=0
net.ipv4.conf.default.rp_filter = 0
++ sudo sysctl -w net.ipv4.conf.eth0.rp_filter=0
net.ipv4.conf.eth0.rp_filter = 0
++ echo 'Displaying current configuration for verification...'
Displaying current configuration for verification...
++ sudo iptables -t nat -L -n -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DNAT       6    --  *      *       0.0.0.0/0            192.168.92.100       mark match 0x65 tcp dpt:443 to:192.168.91.1
    0     0 DNAT       6    --  *      *       0.0.0.0/0            192.168.92.101       mark match 0x66 tcp dpt:443 to:192.168.91.1
    0     0 DNAT       6    --  *      *       0.0.0.0/0            192.168.92.100       mark match 0x65 tcp dpt:80 to:192.168.91.1
    0     0 DNAT       6    --  *      *       0.0.0.0/0            192.168.92.101       mark match 0x66 tcp dpt:80 to:192.168.91.1

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
++ sudo iptables -t mangle -L -n -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MARK       0    --  *      *       0.0.0.0/0            192.168.92.100       MARK set 0x65
    0     0 MARK       0    --  *      *       0.0.0.0/0            192.168.92.101       MARK set 0x66

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MARK       0    --  *      *       0.0.0.0/0            192.168.92.100       MARK set 0x65
    0     0 MARK       0    --  *      *       0.0.0.0/0            192.168.92.101       MARK set 0x66

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
++ sudo ip rule show
0:      from all lookup local
32762:  from all fwmark 0x66 lookup inverter2
32763:  from all fwmark 0x65 lookup inverter1
32764:  from 192.168.92.101 lookup inverter2
32765:  from 192.168.92.100 lookup inverter1
32766:  from all lookup main
32767:  from all lookup default
++ sudo ip route show table 101
192.168.91.0/24 via 192.168.1.250 dev eth0 onlink 
++ sudo ip route show table 102
192.168.91.0/24 via 192.168.1.67 dev eth0 onlin

@jasonacox
Copy link
Author

I wonder if there is a way to do this with virtual interfaces (e.g. eth0:1 to one and eth0:2 to the other)? I'm not anywhere close to a network guru, but hopefully someone in the community is.

@Nexarian
Copy link

@jasonacox Virtual interfaces might work, but I was trying to avoid needing to trigger a large-scale refactor of every use of requests.get or requests.post in all of pypowerwall/dashboard. Ergo: I was simply hoping I could create a virtual IP (like 192.168.92.100/101) and then simply redirect pypowerwall to that using the host parameter.

If you're open to that (adding the parameters that set the interface and/or the source address to the system) that is probably MORE likely to work, but it's a much bigger refactor than the simple ones I've done thus far.

@Nexarian
Copy link

Nexarian commented Jan 5, 2025

@jasonacox I have a fix for this.

Status

The script is still a bit rough/immature. I'm working on figuring out how to make it better and/or integrate it into the server. I also have other variations that work using network namespaces and virtual interfaces, but those seem unnecessary.

Summary

Essentially: Instead of calling 192.168.91.1 directly, set the host in pypowerwall to one of [192.168.92.100, 192.168.92.101] (Subnet choice is arbitrary). Currently my inverters are at [192.168.1.67 , 192.168.92.250] and my raspberry pi is at 192.168.1.225

Steps

  1. Create virtual IP address
  2. Add static route in a unique table
  3. Add DNAT destination rerouting to convert target IP to Tesla internal IP in the OUTPUT (NOT PREROUTING) table.
  4. Add a mangle table entry to MARK anything coming from the virtual IP
  5. Add a rule that anything marked should use the unique table above.
  6. Finally, add a postrouting rule so that this crazy request can find its way back.

This is the reason for this fix that I posted earlier today. Most of TEDAPI worked except firmware version retrieval!

I will update my other PR with this.

Code

Routing Configuration
#!/bin/bash

set -ex

# Function to check if a command succeeded
check_command() {
    if [ $? -ne 0 ]; then
        echo "Error: $1"
        exit 1
    fi
}

# Function to clean up existing rules and configurations
cleanup() {
    echo "Cleaning up existing rules and configurations..."
    
    # Remove virtual IP addresses
    ip addr del 192.168.92.100/24 dev eth0 2>/dev/null || true
    ip addr del 192.168.92.101/24 dev eth0 2>/dev/null || true

    # Flush NAT table
    iptables -t nat -F
    check_command "Failed to flush NAT table"

    # Flush mangle table
    iptables -t mangle -F
    check_command "Failed to flush mangle table"

    # Remove all rules in filter table
    iptables -F
    check_command "Failed to flush filter table"

    # Remove non-default chains
    iptables -X
    check_command "Failed to delete non-default chains"

    # Remove all ip rules (except default)
    ip rule show | grep -v "from all lookup" | cut -d: -f1 | xargs -r -n1 ip rule del prio
    check_command "Failed to remove ip rules"

    # Remove routing tables
    ip route flush table 100 || true
    ip route flush table 101 || true
    check_command "Failed to flush routing tables"

    echo "Cleanup completed successfully."
}

# Main setup function
setup() {
    echo "Setting up routing and NAT rules..."

    # Enable IP forwarding
    echo 1 > /proc/sys/net/ipv4/ip_forward
    check_command "Failed to enable IP forwarding"

    # Add virtual IP addresses
    ip addr add 192.168.92.100/24 dev eth0
    check_command "Failed to add virtual IP 192.168.92.100"
    ip addr add 192.168.92.101/24 dev eth0
    check_command "Failed to add virtual IP 192.168.92.101"

    # Set up routing tables
    ip route add 192.168.91.0/24 via 192.168.1.67 dev eth0 onlink table 100
    check_command "Failed to add route to table 100"
    ip route add 192.168.91.0/24 via 192.168.1.250 dev eth0 onlink table 101
    check_command "Failed to add route to table 101"

    # Set up NAT rules
    iptables -t nat -A OUTPUT -d 192.168.92.100 -j DNAT --to-destination 192.168.91.1
    check_command "Failed to add DNAT rule for 192.168.92.100"
    iptables -t nat -A OUTPUT -d 192.168.92.101 -j DNAT --to-destination 192.168.91.1
    check_command "Failed to add DNAT rule for 192.168.92.101"

    iptables -t nat -A PREROUTING -d 192.168.92.100 -j DNAT --to-destination 192.168.91.1
    check_command "Failed to add DNAT rule for 192.168.92.100"
    iptables -t nat -A PREROUTING -d 192.168.92.101 -j DNAT --to-destination 192.168.91.1
    check_command "Failed to add DNAT rule for 192.168.92.101"

    # Set up packet marking
    iptables -t mangle -A OUTPUT -d 192.168.92.100 -j MARK --set-mark 1
    check_command "Failed to add mark for 192.168.92.100"
    iptables -t mangle -A OUTPUT -d 192.168.92.101 -j MARK --set-mark 2
    check_command "Failed to add mark for 192.168.92.101"

    # Set up ip rules
    ip rule add fwmark 1 table 100
    check_command "Failed to add ip rule for mark 1"
    ip rule add fwmark 2 table 101
    check_command "Failed to add ip rule for mark 2"

    # Set up return traffic NAT
    iptables -t nat -A POSTROUTING -s 192.168.92.100 -d 192.168.91.1 -j SNAT --to-source 192.168.1.225
    check_command "Failed to add return SNAT rule for 192.168.92.100"
    iptables -t nat -A POSTROUTING -s 192.168.92.101 -d 192.168.91.1 -j SNAT --to-source 192.168.1.225
    check_command "Failed to add return SNAT rule for 192.168.92.101"

    echo "Setup completed successfully."
}

# Main execution
if [[ $EUID -ne 0 ]]; then
   echo "This script must be run as root" 
   exit 1
fi

# Cleanup first
cleanup

# Then setup
setup

echo "All operations completed successfully."

@jasonacox
Copy link
Author

Thanks @Nexarian !

@billraff
Copy link

billraff commented May 12, 2025

My PW was updated to 25.2.0 last night and so I see I am no longer getting data in the dashboard. I ran setup to use option 2 for Cloud and data is now being recorded. I'd like to get back to having vitals in the dashboard. I have a RPi4 that was collecting dust in my closet so moved into the garage to be closer to the gateway and the TEG access point. I followed successfully steps 1-3 above and am ready to proceed to modifying the Host. My PW Dashboard is running in Docker on my Synology. In the command -

Linux

sudo ip r add 192.168.91.0/24 via 10.0.1.55

what exactly is happening? What does the via 10.0.1.55 indicate? Since I don't know what that ip refers to I'm a little gun shy to pull the trigger. Thanks for any help.

Edit:

So went ahead and set everything up (even the last step). I'm having issues getting logged into the gateway though when I try option 1 in the setup. It fails and I have to revert to option 2 (cloud). When I connect to the gateway when I have my iPad on the TEG-xx network (192.168.91.1) I get the familiar login and I can log in using last 5 of the gateway password and my email address. But in the setup.sh I've tried the full 10 letters, last 5 letters and it always fails. The login at the gateway no longer has the installer option for me, only customer.

@jasonacox
Copy link
Author

jasonacox commented May 13, 2025

10.0.1.55

This is a bogus placeholder representing the local IP address (LAN) of your Powerwall gateway. If you don't know what it is, you can scan for it:

# install pypowerwall library
pip install pypowerwall

# scan
python -m pypowerwall scan

You can then use that Ip address to create the static route (replace 10.0.1.55 with the address of your Powerwall). You use use option 1 or 4 in setup to access the extended metrics.

Now for the bad news. As of Tesla Powerwall Firmware 25.10.1, the static route we are trying above no longer works. Tesla is blocking access via that method. You will need to direclty connect to the WiFi Access Point on your Powerwall to get the extended metrics (vitals). Here is a way to set up a RPi to do that: jasonacox/Powerwall-Dashboard#607

Good luck!

@billraff
Copy link

Thanks for the help Jason. Guess I thought using the above was a work around to losing the static route since the description made it seem so to my thinking. Not the first time I've been down a dead end. I started using your wonderful Dashboard on a RPi4 and then moved to the NAS. Guess it's one step forward two steps back.

@jasonacox
Copy link
Author

Yes, Tesla makes it... interesting. 😉

This discussion was started when the routing was allowed (and still is until you hit the 25.10.1 upgrade). I'll edit the top to note that.

@jasonacox
Copy link
Author

Community members are reporting using device like https://www.amazon.com/dp/B09N72FMH5?th=1 to help provide the bridge function mentioned in this GIST (instead of using a RPi). See instructions in jasonacox/Powerwall-Dashboard#109 (comment) by @mccahan.

@NikolayActionEngine
Copy link

Thank you Jason & members who reported it, step #4 (Setup NAT) in Raspberry Pi setup is mandatory: https://gist.github.com/jasonacox/91479957d0605248d7eadb919585616c#raspberry-pi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment