Eikkargh / Candy-API-to-JSON

Communicates with Candy/Hoover washer dryers to produce a JSON readable by Home Assistant
6 stars 2 forks source link

Add support for H3DS4965TACBE-80 Washer/Dryer #5

Closed BenGlossop09 closed 1 month ago

BenGlossop09 commented 1 month ago

I've compiled a list of the programs on my machine, for reference they are; PrNm = 15 = High Heat (dry setting) 14 = Low Heat (dry setting) 13 = Dry Wool (dry setting) 12 = Extra Care 11 = All in One 59' 10 = Rapid 14'/30'/44' 9 = Wool & Soft Care 8 = Rinse 7 = Drain & Spin 6 = Synthetic & Colours 5 = Eco 20C 4 = Cottons & Prewash 3 = Eco 40-60 2 = Cottons Wifi Setting = No specific PrNm as far as I can tell

I've added them into the configuration.yaml as follows:

  - sensor:
      - name: 'Washer/Dryer - Program'
        icon: mdi:progress-star
        state: >
          {% set Prog = state_attr('sensor.candy_washer_dryer', 'PrNm') %}
          {% if Prog == "2" %}Cotton
          {% elif Prog == "3" %}Eco 40-60
          {% elif Prog == "4" %}Cotton & Prewash
          {% elif Prog == "9" %}Wool & Soft Care
          {% elif Prog == "10" %}Rapid 14'/30'/44'
          {% elif Prog == "11" %}All in One 59'
          {% elif Prog == "12" %}Extra Care
          {% elif Prog == "13" %}Dry (Wool)
          {% elif Prog == "14" %}Dry (Low Heat)
          {% elif Prog == "15" %}Dry (High Heat)
          {% elif Prog == None %}Off
          {% else %}{{ Prog }}
          {% endif %}

Both before and after making the above changes to the config file, the program entity remains in the 'Off' state at all times. I also added 'Nm' to the state attribute on line 5 of the above code, that didn't change anything. In candy.py 'Pr' and 'PrNm' are set as equals, as far as I understand at least.

Can I get some more information which could shine light on why the program numbers are not being used correctly?

Eikkargh commented 1 month ago

Excellent, thank you for the program codes. Ill create another configuration.yaml to match the settings for your machine.

My notes suggest I added line 54 for testing. You can either comment this line out: if k == 'PrNm': k == 'Pr' Or edit your .yaml command_line sensor attribute Pr instead of PrNm and the template back to:

- sensor:
      - name: 'Washer/Dryer - Program'
        icon: mdi:progress-star
        state: >
          {% set Prog = state_attr('sensor.candy_washer_dryer', 'Pr') %}
          #etc
Eikkargh commented 1 month ago

If you have no luck there are 2 other hoover/ candy integrations on HACS that are more robust than mine. I created this project because neither one worked for my machine at the time. As my first Python project its very much just muddled together in the simplest way possible. It worked so I have not checked the others recently but they are still under development. Candy Simpy-Fi Haier hOn Although I use the Hoover Wizard app both of the above use the same hardware so should work in theory.

BenGlossop09 commented 1 month ago

I managed to get it working! I changed all references of 'Pr' to 'PrNm' and reloaded all yaml (in developer tools). I also have had no luck with Haier hOn. Candy Simpy-Fi works well for me but your solution gives far more information. Screenshot 2024-09-16 094302 configuration.yaml:

command_line:
  - sensor:
      name: 'Candy Washer Dryer'
      scan_interval: 60
      command_timeout: 30
      command: python3 ./pyscript/candy.py
      value_template: '{{ value_json }}'
      json_attributes:
        - WiFiStatus
        - Err
        - MachMd
        - PrNm
        - PrPh
        - Temp
        - SpinSp
        - RemTime
        - DryT
        - DelVal
        - TotalTime
  - sensor:
      - name: 'Washer/Dryer - Program'
        icon: mdi:progress-star
        state: >
          {% set Prog = state_attr('sensor.candy_washer_dryer', 'PrNm') %}

and candy.py

url = 'http://192.168.4.38/http-read.json?encrypted=1'
key = 'mnlkbhjdlbelcgkc'
request_timeout = 10
retries = 3
retry_delay = 2
candyOff = {'WiFiStatus':'1', 'Err':None, 'MachMd':None, 'PrNm':None, 'PrPh':None, 'Temp':None, 'SpinSp':None, 'RemTime':'0', 'DryT':'0', 'DelVal':0, 'TotalTime':'0'}
tries = 0
candy = {}

Further changes I have made; Changed default 'No Error' code to 0 - 0 is no error for my machine. My machine outputs remaining time in seconds so I have added a divide by 60 to Remaining Time and Total Time. Note remaining time shows as xx.x instead of xx minutes, I should add a truncation to solve this.

Outstanding issues; Not much of an issue but the only feature not supported in the PrNm list is the WiFi setting. It doesn't appear to hold a unique value. I have not yet tested what the program entity will output when WiFi is selected and a wash is started - I would assume the machine will output the actual program selected. Rapid 14'/30'/44' do not each hold a unique program so are bundled together. Add commands?

BenGlossop09 commented 1 month ago

Full candy.py for my machine:

#! /usr/bin/env python
import requests
import json
import time

url = 'http://192.168.X.XX/http-read.json?encrypted=1'
key = 'ENCRYPTION KEY HERE'
request_timeout = 10
retries = 3
retry_delay = 2
candyOff = {'WiFiStatus':'1', 'Err':None, 'MachMd':None, 'PrNm':None, 'PrPh':None, 'Temp':None, 'SpinSp':None, 'RemTime':'0', 'DryT':'0', 'DelVal':0, 'TotalTime':'0'}
tries = 0
candy = {}

#extract data
def fetchHex(xurl, xrequest_timeout):
    try:
        candyhex = requests.get(xurl, timeout=xrequest_timeout).text
        return candyhex
    except:
        return None

# convert data to readable text
def convText():
    hexText = fetchHex(url, request_timeout)
    if hexText == None:
        return None
    bytes_object = bytes.fromhex(hexText)
    coded = bytes_object.decode("ASCII")
    return coded

#decode data
def decode(xkey):
    xored = str()
    codedText = convText()
    if codedText == None:
        return None
    repeated_key = (xkey)*((len(codedText) // len(xkey)) + 1)
    for x in range(len(codedText)):
        xored += chr(ord(codedText[x]) ^ ord(repeated_key[x]))
    return xored

# strip and print data
while tries < retries:
    decoded = decode(key)
    if decoded != None:
        decodedDict = json.loads(decoded)
        candyData = decodedDict.get('statusLavatrice')
        for k, v in candyData.items():
            if k[0:3] != "Opt" and k[0:3] != "Rec" and k[0:3] != "Ste" and k[0:3] != "SLe" and k[0:3] != "Che" and k[0:3] != "PrC" and k[0:3] != "Lan" and k[0:3] != "Fil" and k[0:3] != "Det" and k[0:3] != "Sof" and k[0:3] != "DPr" and k[0:3] != "SPr" and k[0:3] != "Wat" and k[0:3] != "rED":
                if k == 'DelVal' and candyData[k] == '255':
                    candy['DelVal'] = '0'
                else:
                    candy[k] = candyData[k]
        TotalTime = int(candy['DelVal']) * 60 + int(candy['RemTime'])
        candy['TotalTime'] = str(TotalTime)
        candyJson = json.dumps(candy, indent = 4)
        break
    tries += 1
    time.sleep(retry_delay)
if tries == retries:
    candyJson = json.dumps(candyOff, indent = 4)
print(candyJson)

Full configuration.yaml here:

command_line:
  - sensor:
      name: 'Candy Washer Dryer'
      scan_interval: 60
      command_timeout: 30
      command: python3 ./pyscript/candy.py
      value_template: '{{ value_json }}'
      json_attributes:
        - WiFiStatus
        - Err
        - MachMd
        - PrNm
        - PrPh
        - Temp
        - SpinSp
        - RemTime
        - DryT
        - DelVal
        - TotalTime

# Configuration for Candy sensors (Washer/Dryer)
template:
#####################
## Other Templates ##
#####################
#   Washer Dryer
  - sensor:
      - name: 'Washer/Dryer - WiFi'
        icon: mdi:washing-machine
        state: >
          {% set WiFi = state_attr('sensor.candy_washer_dryer', 'WiFiStatus') | default(1) %}
          {% if WiFi == '0' %}On
          {% elif WiFi == '1' %}Off
          {% else %}{{ WiFi }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Error'
        icon: mdi:alert-circle
        state: >
          {% set Error = state_attr('sensor.candy_washer_dryer', 'Err') | default(0) %}
          {% if Error == '0' %}No Errors
          {% elif Error is number %}{{ Error }}
          {% else %}Off
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Status'
        icon: mdi:chart-pie
        state: >
          {% set Status = state_attr('sensor.candy_washer_dryer', 'MachMd') %}
          {% if Status == "1" %}Not Started
          {% elif Status == "2" %}Running
          {% elif Status == "3" %}Paused
          {% elif Status == "4" %}Setting Up
          {% elif Status == "5" %}Delayed
          {% elif Status == "7" %}Finished
          {% elif Status == None %}Off
          {% else %}{{ Status }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Program'
        icon: mdi:progress-star
        state: >
          {% set Prog = state_attr('sensor.candy_washer_dryer', 'PrNm') %}
          {% if Prog == "2" %}Cotton
          {% elif Prog == "3" %}Eco 40-60
          {% elif Prog == "4" %}Cotton & Prewash
          {% elif Prog == "9" %}Wool & Soft Care
          {% elif Prog == "10" %}Rapid 14'/30'/44'
          {% elif Prog == "11" %}All in One 59'
          {% elif Prog == "12" %}Extra Care
          {% elif Prog == "13" %}Dry (Wool)
          {% elif Prog == "14" %}Dry (Low Heat)
          {% elif Prog == "15" %}Dry (High Heat)
          {% elif Prog == None %}Off
          {% else %}{{ Prog }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Phase'
        icon: mdi:progress-question
        state: >
          {% set Phase = state_attr('sensor.candy_washer_dryer', 'PrPh') %} 
          {% if Phase == "0" %}Delayed
          {% elif Phase == "1" %}Prewash
          {% elif Phase == "2" %}Wash
          {% elif Phase == "3" %}Rinse
          {% elif Phase == "4" %}Spin/ Drain
          {% elif Phase == "5" %}End
          {% elif Phase == "6" %}Drying
          {% elif Phase == "7" %}Error
          {% elif Phase == "8" %}Steam
          {% elif Phase == "9" %}Goodnight
          {% elif Phase == "10" %}Spin
          {% elif Phase == None %}Off
          {% else %}{{ Phase }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Wash Temp'
        icon: mdi:thermometer-lines
        unit_of_measurement: '°C'
        state: >
          {% set Temp = state_attr('sensor.candy_washer_dryer', 'Temp') %}
          {% if Temp == None %}0
          {% elif Temp == "0" %}0
          {% else %}{{ Temp }}
          {% endif %} 
  - sensor:
      - name: 'Washer/Dryer - Spin Speed'
        icon: mdi:speedometer
        unit_of_measurement: rpm
        state: >
          {% set Spin = state_attr('sensor.candy_washer_dryer', 'SpinSp') %}
          {% if Spin == None %}0
          {% elif  Spin == "0" %}0
          {% else %}{{ (Spin | int) * 100  }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Wash Time'
        icon: mdi:timer
        unit_of_measurement: 'mins'
        state: >
          {% set WashT = state_attr('sensor.candy_washer_dryer', 'RemTime') %}
          {% if WashT == None %}0
          {% else %}{{ (WashT | int) / 60 | round(2) }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Dry Time'
        icon: mdi:clock-time-four
        state: >
          {% set DryT = state_attr('sensor.candy_washer_dryer', 'DryT') %}
          {% if DryT == None %}Off
          {% elif DryT == "0" %}Dry Off
          {% elif DryT == "3" %}Iron Dry
          {% elif DryT == "4" %}Dry Finished
          {% elif DryT == "5" %}Dry Ending
          {% else %}{{ DryT }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Delay Time'
        icon: mdi:timer
        unit_of_measurement: 'hours'
        state: >
          {% set DelT = state_attr('sensor.candy_washer_dryer', 'DelVal') %}
          {% if DelT == None %}0
          {% else %}{{ DelT | int }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Total Time'
        icon: mdi:timer
        state: >
          {% set TotalT = state_attr('sensor.candy_washer_dryer', 'TotalTime') %}
          {% if TotalT == None %}0
          {% else %}{{ (TotalT | int) / 60 | round(2) }}
          {% endif %}
  - sensor:
      - name: 'Washer/Dryer - Max Delay'
        icon: mdi:timer
        state: >
          {% set Status = states('sensor.washer_dryer_wifi') %}
          {% set Delay = states('sensor.washer_dryer_delay_time') | int(default=0) * 60 %}
          {% set DelayMax = states('sensor.washer_dryer_max_delay') | int(default=0) %}
          {% set Total = states('sensor.washer_dryer_total_time') | int %}
          {% if Total == 0 %}0
          {% elif Status == 'Off' %}{{ DelayMax }}
          {% elif DelayMax >= Delay %}{{ DelayMax }}
          {% else %}{{ Delay }}
          {% endif %}              
  - sensor:
      - name: 'Washer/Dryer - Max Time'
        icon: mdi:timer
        state: >
          {% set Status = states('sensor.washer_dryer_wifi') %}
          {% set Total = states('sensor.washer_dryer_total_time') | int(default=0) %}
          {% set TMax = states('sensor.washer_dryer_max_time') | int(default=0) %}
          {% set DelayMax = states('sensor.washer_dryer_delay_max') | int(default=0) %}
          {% if Total == 0 %}0
          {% elif Status == 'Off' %}{{ TMax }}
          {% elif Total <= (TMax - DelayMax) %}{{ TMax }}
          {% else %}{{ Total }}
          {% endif %}
Eikkargh commented 1 month ago

Excellent. Glad you got it working. I shall add those changes today.

Not much of an issue but the only feature not supported in the PrNm list is the WiFi setting. It doesn't appear to hold a unique value. I have not yet tested what the program entity will output when WiFi is selected and a wash is started - I would assume the machine will output the actual program selected.

I did start playing about with this but found I the Pr/PrNm was whatever was set through the app. I am not sure what key reports the WiFi control state.

Rapid 14'/30'/44' do not each hold a unique program so are bundled together.

My machine doesnt have multiple options on a setting so unable to test this myself. Looking at your full JSON response the key for this could be "PrCode": "122". If you remove this: and k[0:3] != "PrC" from exclude list on line 50 of candy.py and add - PrCode to the json_attribute or your command_line sensor. Assuming you are still under 255 characters you should be able to see if this keys value corresponds to your rapid setting.

Add commands?

I did start working on another python script to handle commands but kept running into problems. I have yet to find any official documentation on the API that would make this a lot easier. Sniffing packets got me some information but duplicating the signal in console failed. Ill take a look at the HACS candy integration see if they have solved this, last I looked they were having they same trouble. Its not a huge priority for me but I can see use cases with blackouts/ solar/ battery/ electric cost changes.

Ill open issues for all your requests and see what can be done.