Skip to content

Commit

Permalink
Merge pull request #69 from ivanfmartinez/juicebox_commands
Browse files Browse the repository at this point in the history
first version that can control current locally without enelx servers
  • Loading branch information
snicker authored Jan 20, 2025
2 parents 57cdc1f + 9cbead2 commit 50e5fbe
Show file tree
Hide file tree
Showing 16 changed files with 1,676 additions and 177 deletions.
28 changes: 28 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,31 @@ Juice Pass Proxy
Copyright 2024 Juice Rescue

This product includes software developed by Juice Rescue.


#
# Some parts of the code based on https://github.com/philipkocanda/juicebox-protocol
#

MIT License

Copyright (c) 2024 Philip Kocanda

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

97 changes: 93 additions & 4 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Variable | Required | Description & Default |
**MQTT_USER** | No |
**MQTT_PASS** | No |
**MQTT_DISCOVERY_PREFIX** | No | homeassistant
**LOG_LOC** | No | /log (use **none** to disable log to file)

<details>
<summary><h3>Less Common Docker Environment Variables</h3></summary>
Expand All @@ -118,9 +119,10 @@ Variable | Required | Description & Default |
**DEVICE_NAME** | No | JuiceBox
**DEBUG** | No | false
**EXPERIMENTAL** | No | Default: false. Enables additional entities in Home Assistant that are in in development or can be used toward developing the ability to send commands to a JuiceBox
**IGNORE_ENELX** | No | Default: false. If true, will not send commands received from EnelX to the JuiceBox nor send outgoing information from the JuiceBox to EnelX
**IGNORE_ENELX** | No | Default: false. If true, will not send commands received from EnelX to the JuiceBox nor send outgoing information from the JuiceBox to
EnelX, to use local control this option should be true
**TELNET_TIMEOUT** | No | Default: 30. Timeout in seconds for telnet operations.
**JUICEBOX_ID** | No | If not defined, will attempt to get the JuiceBox ID using telnet.
**JUICEBOX_ID** | No | If not defined, will attempt to get the JuiceBox ID using telnet, don't use this if you are testing multiple devices.
**LOCAL_IP**<br><br>_Deprecated Variable: SRC_ | No | If not defined, will attempt to get the Local Docker IP. Can optionally define port (ex. 127.0.0.1:8047). If unsuccessful, will default to 127.0.0.1.
**LOCAL_PORT** | No | Local port for JuicePass Proxy to listen on. If not defined, will use the EnelX Port.
**ENELX_IP**<br><br>_Deprecated Variable: DST_ | No | If not defined, will attempt to get the IP of the EnelX Server. If unsuccessful, will default to 54.161.185.130. Can optionally define port (ex. 54.161.185.130:8047). If defined, only use the IP address of the EnelX Server and not the fully qualified domain name to avoid DNS lookup loops.
Expand Down Expand Up @@ -169,6 +171,8 @@ options:
--ignore_enelx If set, will not send commands received from EnelX to
the JuiceBox nor send outgoing information from the
JuiceBox to EnelX
--tp PORT, --telnet_port PORT
Telnet PORT (default: 2000)
--telnet_timeout SECONDS
Timeout in seconds for Telnet operations (default: 30)
--juicebox_id ID JuiceBox ID. If not defined, will obtain it
Expand All @@ -191,7 +195,7 @@ _For `--enelx_ip`, only use the IP address of the EnelX Server and **not** the f

## Getting EnelX Server IPs

To get the destination IP:Port of the EnelX server, telnet into your JuiceBox device:
To get the destination IP:Port of the EnelX server, telnet into your JuiceBox device and use the [list](https://docs.silabs.com/gecko-os/4/standard/4.2/cmd/commands#stream-list) command:
`$ telnet 192.168.x.x 2000`
and type the `list` command:

Expand All @@ -202,6 +206,91 @@ list
# 1 UDPC juicenet-udp-prod3-usa.enelx.com:8047 (26674)
```

The address is in the `UDPC` line. Run, `ping`, `nslookup`, or similar command to determine the IP.
The address is in the `UDPC` line. Run, `ping`, `nslookup`, or similar command to determine the IP. The following [network_lookup](https://docs.silabs.com/gecko-os/4/standard/4.2/cmd/commands#network-lookup) command can be run in JuiceBox telnet to look it up while still connected:
```
network_lookup juicenet-udp-prod3-usa.enelx.com
54.161.185.130
network_lookup jvb1.emotorwerks.com
158.47.1.128
```

As of November, 2023: `juicenet-udp-prod3-usa.enelx.com` = `54.161.185.130`.

## Important information
- This proxy is made using effort from owners that found information and made packet capture to reverse enginner the protocol used by the devices
- There are many different firmware versions found
- some accept telnet, some others not
- Different protocol versions are found
- We cannot assure that this will work will all versions
- If this does not work with your device you must provide :
- logs (and if possible packet captures) with messages that are send to/from your device
- docker enviroment configuration used or juicepassproxy command line parameters
- if your device still works with ENELX servers but not with juicepass :
- a packet capture will provide usefull information to understand what are the differences that are not being considered yet
- Sometimes it takes a while to stabilize, if you are changing between ENEL X and JPP let it running for some minutes before testing

## juicepassproxy important behaviours to understand
- For devices that uses the protocol version v07 the juicepassproxy will only start talking with device after 6 minutes to make sure it gets the correct offline current in the device.
- when defining the MQTT entities that are show on homeassistant juicepassproxy will define a max_current value, on the first time it starts it will use 48A for this value, after receiving the device rating the value will be stored on configuration and at next start will be used as maximum to allow the correct range on homeassistant

## Controlling Charging current

- **Max Current (Offline/Wanted)**
- Control maximum current that device will charge when offline (not connected to juicebox or Enel X)
- 2024-06 tested on device which send protocol v09u it changes **Max Charging Current** to this value around 5 minutes after not receiving messages from proxy
- Stored on EEPROM - https://github.com/snicker/juicepassproxy/issues/39#issuecomment-2002312548
- Because of this **don't change that value many times**, as any EEPROM has a lifespan based on writes and the *Max Charging Current* will make possible to control the Current for Charging

- **Max Current (Offline/Device)**
- The value that are sent from the juicebox device indicating what will the offline charge current

- **Max Current (Online/Wanted)**
- Control the Current that Juicebox provides to the vehicle when connected to server
- Can be used for example to control charging based on Solar Power generation
- As suggestion check for changes at 3-5 minutes intervals
- this give time for stabilizations on charging and energy generation
- this interval was tested with old ENEL X API integration and now with juicepassproxy responding to juicebox
- Putting 0 pauses the charging
- Pausing will reset the session energy value
- This may affect the lifespan of internal contactor if paused/restarted too many times
- Some cars can have different behaviours
- Bolt 2022 (Brazil) checks the Current at connection
- if the value is less equal than 10 A it will consider that is using a portable charger and cannot accept Current changes greater than 10A later
- if the value is 0 A (Pause), it will show a charging error on dashboard but will start charging when value goes to 6A or over

- **Max Current (Online/Device)**
- The value that are sent from the juicebox device indicating what is the online charge current


- Warning about offline / online
- Apparently some devices consider offline as a maximum that can be used on device, and even if you put online over that it will consider the minimum value
- https://github.com/JuiceRescue/juicepassproxy/pull/69#issuecomment-2408423204
- Tests on one JB 2.x / v09u indicates that the online value can be over the offline value for charging
- This will allow safe usage of load-balancing logic from a server and use lower values for safety
- If you have one of this devices and have limited circuit you must respect your circuit limits when changing the online value

## Energy
- **Energy Session**
- the Juicebox device reset this value when car changes from **Charging** to **Plugged In** State

## Multiple JuiceBoxes
- Multiple instances of JPP must be executed, one per JuiceBox.
- Each JPP instance should specify the following parameters in addition to the basic parameters.
- **--name** - each should use a different name, because this is the identifier of MQTT topic
- **--juicebox_id** - defining this disable the telnet and will start faster, must be the correct serial of each device
- **--local_port** - each needs to use their own port but make sure the UDP 8042 redirection rule matches the destination port
- **--config_loc** - each needs their own directory
- Future versions can be able to work with multiple devices : https://github.com/JuiceRescue/juicepassproxy/issues/102


## Configuration file
- You can configure initial state of mqtt entities :
- **ENTITY_initial_state** or **SERIAL_ENTITY_initial_state**
- **current_max_offline_set_initial_state** can be used for device that does not send current_max_offline value on status messages (v07 protocol) and do faster startup

## Upgrading from older versions if you have any problem with wrong entities on Homeassistant
- Stop juicepassproxy
- Remove old configuration on MQTT, using mosquitto_sub or any other MQTT client
- **mosquitto_sub -t "homeassistant/+/JuiceBox/+/config" -v --remove-retained**
- Remove Juicebox device on Homeassistant
- Start juicepassproxy
5 changes: 3 additions & 2 deletions const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging

# Will auto-update based on GitHub release tag
VERSION = "v0.3.1"
VERSION = "v0.5.0"

CONF_YAML = "juicepassproxy.yaml"

Expand All @@ -20,7 +20,8 @@
DEFAULT_MQTT_PORT = "1883"
DEFAULT_MQTT_DISCOVERY_PREFIX = "homeassistant"
DEFAULT_DEVICE_NAME = "JuiceBox"
DEFAULT_TELNET_TIMEOUT = 30
DEFAULT_TELNET_PORT = "2000"
DEFAULT_TELNET_TIMEOUT = "30"

# How many times to fully restart JPP before exiting
MAX_JPP_LOOP = 10
Expand Down
7 changes: 6 additions & 1 deletion docker_entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ if [[ ! -z "${TELNET_TIMEOUT}" ]]; then
JPP_STRING+=" --telnet_timeout ${TELNET_TIMEOUT}"
fi
JPP_STRING+=" --config_loc /config"
JPP_STRING+=" --log_loc /log"
if [[ -v LOG_LOC ]]; then
logger INFO "LOG_LOC: ${LOG_LOC}"
JPP_STRING+=" --log_loc ${LOG_LOC}"
else
JPP_STRING+=" --log_loc /log"
fi
logger INFO "DEBUG: ${DEBUG}"
if $DEBUG; then
JPP_STRING+=" --debug"
Expand Down
89 changes: 89 additions & 0 deletions juicebox_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import yaml
from pathlib import Path
import logging

from const import (
CONF_YAML,
)

_LOGGER = logging.getLogger(__name__)

class JuiceboxConfig:


def __init__(self, config_loc, filename=CONF_YAML):
self.config_loc = Path(config_loc)
self.config_loc.mkdir(parents=True, exist_ok=True)
self.config_loc = self.config_loc.joinpath(filename)
self.config_loc.touch(exist_ok=True)
_LOGGER.info(f"config_loc: {self.config_loc}")
self._config = {}
self._changed = False


async def load(self):
config = {}
try:
_LOGGER.info(f"Reading config from {self.config_loc}")
with open(self.config_loc, "r") as file:
config = yaml.safe_load(file)
except Exception as e:
_LOGGER.warning(f"Can't load {self.config_loc}. ({e.__class__.__qualname__}: {e})")
if not config:
config = {}
self._config = config

async def write(self):
try:
_LOGGER.info(f"Writing config to {self.config_loc}")
with open(self.config_loc, "w") as file:
yaml.dump(self._config, file)
self._changed = False
return True
except Exception as e:
_LOGGER.warning(
f"Can't write to {self.config_loc}. ({e.__class__.__qualname__}: {e})"
)
return False


async def write_if_changed(self):
if self._changed:
return await self.write()
return True

def get(self, key, default):
return self._config.get(key, default)

# Get device specific configuration, if not found try to use global parameter
def get_device(self, device, key, default):
return self._config.get(device +"_" + key, self._config.get(key, default))

def update(self, data):
# TODO detect changes
return self._config.update(data)

def update_value(self, key, value):
if self._config.get(key, None) != value:
self.update({ key : value })
self._changed = True

def update_device_value(self, device, key, value):
self.update_value(device + "_" + key, value)


def pop(self, key):
if key in self._config:
self._config.pop(key, None)
self._changed = True

def is_changed(self):
return self._changed








52 changes: 52 additions & 0 deletions juicebox_crc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#
# Original code : https://github.com/philipkocanda/juicebox-protocol
#
class JuiceboxCRC:
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

def __init__(self, payload: str) -> None:
self.payload = payload
pass

def integer(self) -> int:
return self.crc(self.payload)


def base35(self) -> str:
return self.base35encode(self.integer())


def inspect(self) -> dict:
return {
"payload": self.payload,
"base35": self.base35(),
"integer": self.integer(),
}

def base35encode(self, number: int) -> str:
base35 = ""

# Sometimes it ends with 0 and the juicebox CRC should have 3 characters
while (number > 1) or (len(base35) < 3):
number, i = divmod(number, 35)
if i == 24:
i = 35
base35 = base35 + self.ALPHABET[i]

return base35


def base35decode(self, number: str) -> int:
decimal = 0
for i, s in enumerate(reversed(number)):
decimal += self.ALPHABET.index(s) * (35**i)
return decimal


def crc(self, data: str) -> int:
h = 0
for s in data:
h ^= (h << 5) + (h >> 2) + ord(s)
h &= 0xFFFF
return h

13 changes: 13 additions & 0 deletions juicebox_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#
# Original code : https://github.com/philipkocanda/juicebox-protocol
#
class JuiceboxException(Exception):
"Generic exception class for this library"
pass

class JuiceboxInvalidMessageFormat(JuiceboxException):
pass


class JuiceboxCRCError(JuiceboxException):
pass
Loading

0 comments on commit 50e5fbe

Please sign in to comment.