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

first version that can control current locally without enelx servers #69

Open
wants to merge 62 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
2720848
first version that can control current locally without enelx servers
ivanfmartinez Jun 1, 2024
5ad8db2
more decoding and comments
ivanfmartinez Jun 2, 2024
9886c3a
adjust currents based on @FalconFour comments on https://github.com/s…
ivanfmartinez Jun 2, 2024
a2b77bc
workaround unknown problem when setting value from HA
ivanfmartinez Jun 2, 2024
6342e47
consider the existence of new encrypted messages
ivanfmartinez Jun 5, 2024
21933ab
better message handling
ivanfmartinez Jun 10, 2024
e2cb9c6
charging control information on README
ivanfmartinez Jun 13, 2024
1529b3c
add support for a different version of protocol
ivanfmartinez Jun 19, 2024
01c2301
added status and voltage processing to the message class, add more su…
ivanfmartinez Jun 23, 2024
1fbaee6
add act_as_server switch to enable/disable responses from proxy, add …
ivanfmartinez Jun 29, 2024
0a9f355
add support for v07 message without letter
ivanfmartinez Oct 3, 2024
4add525
workaround v07 message that does not send offline max current
ivanfmartinez Oct 3, 2024
bbfdbcb
remove comment that are already explained on README
ivanfmartinez Oct 4, 2024
dd335da
dont use save command
ivanfmartinez Oct 5, 2024
b881a04
make possible to create command messages without all values
ivanfmartinez Oct 5, 2024
e6052f1
set only defined parameters for command message
ivanfmartinez Oct 5, 2024
2a7a477
basic decoding for debug messages from juicebox
ivanfmartinez Oct 6, 2024
c42dc53
convert more values to integer and add to test
ivanfmartinez Oct 6, 2024
7f0be18
temperature conversion as on original code
ivanfmartinez Oct 6, 2024
eeb27c2
change checksum to CRC, start using message class to send data to MQTT
ivanfmartinez Oct 6, 2024
3bb783d
more debug samples
ivanfmartinez Oct 6, 2024
c6baaff
try to solve a racing condition when setting parameter and juicebox i…
ivanfmartinez Oct 6, 2024
d3121f4
only send command messages for received status messages
ivanfmartinez Oct 7, 2024
5845b2a
more message decoding changes and tests
ivanfmartinez Oct 8, 2024
3bbe188
round some values
ivanfmartinez Oct 8, 2024
f00cde2
split the online/offline entities for wanted value and device value
ivanfmartinez Oct 9, 2024
0467068
refactor message decoding and setting for the wanted entities
ivanfmartinez Oct 9, 2024
6f63144
missing from last commit
ivanfmartinez Oct 9, 2024
9d7d678
applied telnet patch from @atc99
ivanfmartinez Oct 10, 2024
09bdb62
indicate error message if serial was changed, and more information o …
ivanfmartinez Oct 10, 2024
164e853
refactor config to allow getting device specific configuration
ivanfmartinez Oct 10, 2024
ec275ad
added calculated power like before
ivanfmartinez Oct 10, 2024
fbee608
dont need the current_rating, just have to wait more than 5 minutes t…
ivanfmartinez Oct 10, 2024
aaebb41
now storing device important values on config file
ivanfmartinez Oct 10, 2024
5856a44
add way to change the reuse_port on commandline or configuration file
ivanfmartinez Oct 11, 2024
7733983
information about how to clean JPP/HA MQTT config
ivanfmartinez Oct 19, 2024
5110d15
more v07 information
ivanfmartinez Oct 19, 2024
334c3ea
information about max_current on MQTT
ivanfmartinez Oct 19, 2024
0932ec5
some refatoring after including test message from issue 84
ivanfmartinez Oct 20, 2024
95c22a8
more generic way to add calculated values
ivanfmartinez Oct 20, 2024
f1fda08
information about p parameter
ivanfmartinez Oct 24, 2024
1e5c77f
power_factor decoding
ivanfmartinez Oct 24, 2024
55f1065
added philipkocanda LICENSE on NOTICE file
ivanfmartinez Nov 4, 2024
cdf65db
some extra information about device behaviour
ivanfmartinez Nov 4, 2024
c688fb6
add Juice Rescue text
ivanfmartinez Nov 4, 2024
90b9e03
Merge branch 'master' into juicebox_commands
ivanfmartinez Nov 4, 2024
56c700c
allow to disable log to file
ivanfmartinez Nov 10, 2024
3d83564
Merge branch 'juicebox_commands' of https://github.com/ivanfmartinez/…
ivanfmartinez Nov 10, 2024
9c4bd43
change version as the branch is updated with master
ivanfmartinez Nov 10, 2024
70a8db3
better message checking after testing data from issue #111
ivanfmartinez Nov 10, 2024
ee2a14c
correct call the config method with defaultValue
ivanfmartinez Nov 11, 2024
92156dd
default log_path as string
ivanfmartinez Nov 11, 2024
845c5a9
indicate the procedure to return juicebox to unencrypted messages whe…
ivanfmartinez Nov 15, 2024
780fe78
better checking for v08 encrypted message
ivanfmartinez Nov 16, 2024
6f00f9c
compatibility with old ha-mqtt-discoverable that was forced on #76
ivanfmartinez Nov 17, 2024
2a54b9d
more changes to dont generate exceptions on encrypted messages, remov…
ivanfmartinez Nov 17, 2024
48cd2d8
better CRC processing and log
ivanfmartinez Nov 17, 2024
0727162
warn user when trying to send commands to juicebox without ignoring e…
ivanfmartinez Nov 17, 2024
97bf3ba
when udpc is incorrect show connections on debug
ivanfmartinez Nov 17, 2024
bf430c2
indicate the need of ignore_enelx to local control
ivanfmartinez Nov 17, 2024
3fa10f4
correct test method name, thanks to SeaMonster :-)
ivanfmartinez Nov 19, 2024
9cbead2
text corrections
ivanfmartinez Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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