mathieu-mp / aio-intex-spa Goto Github PK
View Code? Open in Web Editor NEWPython client for Intex Spa wifi interface
Home Page: https://pypi.org/project/aio-intex-spa/
License: MIT License
Python client for Intex Spa wifi interface
Home Page: https://pypi.org/project/aio-intex-spa/
License: MIT License
Describe the bug
_async_handle_intent does not return status when intent
expected_state
is already met.
Fixes mathieu-mp/homeassistant-intex-spa#26
Proposed solution
Make _async_handle_intent always return a status
cd27a20
After power outtage, i cannot send commands/update state of spa. All entities are available, but when i try to change something, it says "errno 104".
I tried to capture packets using tshark, but there are no packet generated
This error originated from a custom integration.
Logger: homeassistant
Source: custom_components/intex_spa/__init__.py:71
Integration: Intex Spa (documentation, issues)
First occurred: 11:45:59 AM (213 occurrences)
Last logged: 1:31:59 PM
Error doing job: Task exception was never retrieved
Traceback (most recent call last):
File "/config/custom_components/intex_spa/__init__.py", line 67, in _async_update_data
return await self.api.async_update_status()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 144, in async_update_status
return await self._async_handle_intent("status")
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 123, in _async_handle_intent
await self.network.async_force_reconnect()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 82, in async_force_reconnect
await self._async_disconnect()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 74, in _async_disconnect
await self.writer.wait_closed()
File "/usr/local/lib/python3.10/asyncio/streams.py", line 344, in wait_closed
await self._protocol._get_close_waiter(self)
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 101, in _async_handle_intent
received_bytes = await self.network.async_receive()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 100, in async_receive
response_as_bytes = await self.reader.readline()
File "/usr/local/lib/python3.10/asyncio/streams.py", line 525, in readline
line = await self.readuntil(sep)
File "/usr/local/lib/python3.10/asyncio/streams.py", line 617, in readuntil
await self._wait_for_data('readuntil')
File "/usr/local/lib/python3.10/asyncio/streams.py", line 502, in _wait_for_data
await self._waiter
File "/usr/local/lib/python3.10/asyncio/selector_events.py", line 854, in _read_ready__data_received
data = self._sock.recv(self.max_size)
ConnectionResetError: [Errno 104] Connection reset by peer
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 151, in _handle_refresh_interval
await self._async_refresh(log_failures=True, scheduled=True)
File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 258, in _async_refresh
raise err
File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 205, in _async_refresh
self.data = await self._async_update_data()
File "/config/custom_components/intex_spa/__init__.py", line 71, in _async_update_data
raise NotImplementedError from exception
NotImplementedError
shortened this, cannot fit to 65k characters
Logger: homeassistant.components.websocket_api.http.connection
Source: custom_components/intex_spa/__init__.py:67
Integration: Home Assistant WebSocket API (documentation, issues)
First occurred: 1:18:29 PM (9 occurrences)
Last logged: 1:28:09 PM
[140663695691392] [Errno 104] Connection reset by peer
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 193, in handle_call_service
await hass.services.async_call(
File "/usr/src/homeassistant/homeassistant/core.py", line 1713, in async_call
task.result()
File "/usr/src/homeassistant/homeassistant/core.py", line 1750, in _execute_service
await cast(Callable[[ServiceCall], Awaitable[None]], handler.job.target)(
File "/usr/src/homeassistant/homeassistant/helpers/entity_component.py", line 204, in handle_service
await service.entity_service_call(
File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 680, in entity_service_call
future.result() # pop exception if have
File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 930, in async_request_call
await coro
File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 717, in _handle_entity_call
await result
File "/config/custom_components/intex_spa/switch.py", line 93, in async_turn_off
status = await self.coordinator.api.async_set(self._switch_type, False)
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 157, in async_set
return await self._async_handle_intent(parameter, expected_state)
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 77, in _async_handle_intent
await self.async_update_status()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 144, in async_update_status
return await self._async_handle_intent("status")
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 123, in _async_handle_intent
await self.network.async_force_reconnect()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 82, in async_force_reconnect
await self._async_disconnect()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 74, in _async_disconnect
await self.writer.wait_closed()
File "/usr/local/lib/python3.10/asyncio/streams.py", line 344, in wait_closed
await self._protocol._get_close_waiter(self)
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 98, in _async_handle_intent
await self.network.async_send(query.request_bytes)
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 91, in async_send
await self._async_disconnect()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 74, in _async_disconnect
await self.writer.wait_closed()
File "/usr/local/lib/python3.10/asyncio/streams.py", line 344, in wait_closed
await self._protocol._get_close_waiter(self)
File "/config/custom_components/intex_spa/__init__.py", line 67, in _async_update_data
return await self.api.async_update_status()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 144, in async_update_status
return await self._async_handle_intent("status")
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 123, in _async_handle_intent
await self.network.async_force_reconnect()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 82, in async_force_reconnect
await self._async_disconnect()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 74, in _async_disconnect
await self.writer.wait_closed()
File "/usr/local/lib/python3.10/asyncio/streams.py", line 344, in wait_closed
await self._protocol._get_close_waiter(self)
...
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 82, in async_force_reconnect
await self._async_disconnect()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 74, in _async_disconnect
await self.writer.wait_closed()
File "/usr/local/lib/python3.10/asyncio/streams.py", line 344, in wait_closed
await self._protocol._get_close_waiter(self)
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 101, in _async_handle_intent
received_bytes = await self.network.async_receive()
File "/usr/local/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 100, in async_receive
response_as_bytes = await self.reader.readline()
File "/usr/local/lib/python3.10/asyncio/streams.py", line 525, in readline
line = await self.readuntil(sep)
File "/usr/local/lib/python3.10/asyncio/streams.py", line 617, in readuntil
await self._wait_for_data('readuntil')
File "/usr/local/lib/python3.10/asyncio/streams.py", line 502, in _wait_for_data
await self._waiter
File "/usr/local/lib/python3.10/asyncio/selector_events.py", line 854, in _read_ready__data_received
data = self._sock.recv(self.max_size)
ConnectionResetError: [Errno 104] Connection reset by peer
Hey!
as i discovered that original intex app scheduling was somewhat flaky (sometimes it worked other times not) i decided to implement scheduling of filter running thrugh my local HA.
However i have noticed that sending ON command for filtering turns it on for 2 hours and it shuts off automatically after that (the display shows 02h when sending filter on command).
in contrast turning filtering on the control panel offers 2,4 or 6h running time.
Is it possible that there is also different remote command for running time and could be implemented?
thx
Fix GitHub actions for repo
Triggering async_set_filter
enables filter with a 2 hours timer, instead of enabling filter for good.
Raise a proper exception if _raw_status is not set or empty when IntexSpaStatus properties are read
@Elkropac original post:
I was trying to figure out, what is returned in status
, when you cannot connect to SPA and if it needs to be handled in some update method instead of blindly updating status of coordinator data
Originally posted by @Elkropac in mathieu-mp/homeassistant-intex-spa#10 (comment)
Hi, i wanted to see, if error is presented in code returned as status.
My SPA now says , that it has E81 (no power) on display, and
DEBUG:intex_spa.intex_spa_network_layer:Receiving bytes from the spa: b'{"sid":"16555066810742","data":"FFFF110F010001B5000000008080801B00008B","result":"ok","type":2}\n'
DEBUG:intex_spa.intex_spa_status:Spa status: '{'power': False, 'filter': False, 'heater': False, 'jets': False, 'bubbles': False, 'sanitizer': False, 'unit': '°C', 'current_temp': 181, 'preset_temp': 27}'
after restoring power
DEBUG:intex_spa.intex_spa_network_layer:Receiving bytes from the spa: b'{"sid":"16555066641209","data":"FFFF110F01000016000000008080801B00002C","result":"ok","type":2}\n'
DEBUG:intex_spa.intex_spa_status:Spa status: '{'power': False, 'filter': False, 'heater': False, 'jets': False, 'bubbles': False, 'sanitizer': False, 'unit': '°C', 'current_temp': 22, 'preset_temp': 27}'
so the error is mixed in the current_temp.
Don't have time to debug now, just reporting
JSONDecodeError to catch when response cannot be received
2022-06-20 17:17:39 ERROR (MainThread) [custom_components.intex_spa] Error fetching intex_spa data:
2022-06-20 17:18:09 ERROR (MainThread) [homeassistant.core] Error executing service: <ServiceCall switch.turn_on (c:01G611X014FMMB0MVRR50BTW7K): entity_id=['switch.spa_power']>
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/core.py", line 1722, in catch_exceptions
await coro_or_task
File "/usr/src/homeassistant/homeassistant/core.py", line 1741, in _execute_service
await cast(Callable[[ServiceCall], Awaitable[None]], handler.job.target)(
File "/usr/src/homeassistant/homeassistant/helpers/entity_component.py", line 204, in handle_service
await service.entity_service_call(
File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 680, in entity_service_call
future.result() # pop exception if have
File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 964, in async_request_call
await coro
File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 717, in _handle_entity_call
await result
File "/config/custom_components/intex_spa/switch.py", line 86, in async_turn_on
status = await self.coordinator.api.async_set(self._switch_type, True)
File "/usr/local/lib/python3.9/site-packages/intex_spa/intex_spa.py", line 152, in async_set
return await self._async_handle_intent(parameter, expected_state)
File "/usr/local/lib/python3.9/site-packages/intex_spa/intex_spa.py", line 83, in _async_handle_intent
await self.async_update_status()
File "/usr/local/lib/python3.9/site-packages/intex_spa/intex_spa.py", line 139, in async_update_status
return await self._async_handle_intent("status")
File "/usr/local/lib/python3.9/site-packages/intex_spa/intex_spa.py", line 106, in _async_handle_intent
query.render_response_status(received_bytes)
File "/usr/local/lib/python3.9/site-packages/intex_spa/intex_spa_query.py", line 87, in render_response_status
response = json.loads(received_bytes.decode())
File "/usr/local/lib/python3.9/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
File "/usr/local/lib/python3.9/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/usr/local/lib/python3.9/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
I added firewall rule between my dev host and SPA. Then switched the power switch. First line of log pops up almost instatly, exception shows after 30s timeout. My firewall is set to REJECT packets, not to DROP
Originally posted by @Elkropac in mathieu-mp/homeassistant-intex-spa#10 (comment)
Hi,
can I use it also for a raspi and node red?
I use the stuff from Claudio, but only the status stuff is working but I need the action commands.
Thanks.
cheers, Peter
Describe the bug
The package does not support multiple simultaneous requests.
Exception is raised
log from Home Assistant
2022-06-20 16:33:43 ERROR (MainThread) [homeassistant.components.websocket_api.http.connection] [139669599812720] readuntil() called while another coroutine is already waiting for incoming data
Traceback (most recent call last):
File "/usr/local/python/lib/python3.10/site-packages/homeassistant/components/websocket_api/commands.py", line 193, in handle_call_service
await hass.services.async_call(
File "/usr/local/python/lib/python3.10/site-packages/homeassistant/core.py", line 1706, in async_call
task.result()
File "/usr/local/python/lib/python3.10/site-packages/homeassistant/core.py", line 1743, in _execute_service
await cast(Callable[[ServiceCall], Awaitable[None]], handler.job.target)(
File "/usr/local/python/lib/python3.10/site-packages/homeassistant/helpers/entity_component.py", line 204, in handle_service
await service.entity_service_call(
File "/usr/local/python/lib/python3.10/site-packages/homeassistant/helpers/service.py", line 680, in entity_service_call
future.result() # pop exception if have
File "/usr/local/python/lib/python3.10/site-packages/homeassistant/helpers/entity.py", line 900, in async_request_call
await coro
File "/usr/local/python/lib/python3.10/site-packages/homeassistant/helpers/service.py", line 717, in _handle_entity_call
await result
File "/config/custom_components/intex_spa/switch.py", line 87, in async_turn_off
status = await self.coordinator.api.async_set(self._switch_type, False)
File "/usr/local/python/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 152, in async_set
return await self._async_handle_intent(parameter, expected_state)
File "/usr/local/python/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 83, in _async_handle_intent
await self.async_update_status()
File "/usr/local/python/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 139, in async_update_status
return await self._async_handle_intent("status")
File "/usr/local/python/lib/python3.10/site-packages/intex_spa/intex_spa.py", line 104, in _async_handle_intent
received_bytes = await self.network.async_receive()
File "/usr/local/python/lib/python3.10/site-packages/intex_spa/intex_spa_network_layer.py", line 82, in async_receive
response_as_bytes = await self.reader.readline()
File "/usr/local/python/lib/python3.10/asyncio/streams.py", line 525, in readline
line = await self.readuntil(sep)
File "/usr/local/python/lib/python3.10/asyncio/streams.py", line 617, in readuntil
await self._wait_for_data('readuntil')
File "/usr/local/python/lib/python3.10/asyncio/streams.py", line 488, in _wait_for_data
raise RuntimeError(
RuntimeError: readuntil() called while another coroutine is already waiting for incoming data
Hello, (thanks google translation, I'm French without great English skills!), I would like to know if your plugin could be installed on a raspberry with Domoticz? If so, could you show me how to do it? Where to start ? knowing that I am not an expert like you but a simple guy who would like to enjoy his spa via his home automation!
Thank you for your understanding.
Sincerely
ps: I hope not to disturb the project, I did not know how to contact you.
Hi Mathieu,
Nice job.
Jeedom integration done and quite easy.
Will it be possible to get battery info at some point?
Cheers,
Patrice
I have a cron job running that records the temperature and heats overnight. I am consistently getting exceptions thrown for a period because the checksum doesn't match.
I took the output from the exception below and ran it through the checksum as int function and it came back as zero, as you can see the spa returned it as 0xFF. If this is a legitimate issue, I am surprised it hasn't come up before although I am using Farenheit. Calling the function and ignoring the assertion for the assertion for the checksum the status below is reported. The spa I have is an SC-WF10.
I tried replicating the issue today with the same settings but was unable to get the code to fail. I have posted the failing bytes returned and the passing bytes returned below:
FFFF110F0107006400000085808085670000FF
FFFF110F01070064000000008080806700008A
{'power': True, 'filter': True, 'heater': True, 'jets': False, 'bubbles': False, 'sanitizer': False, 'unit': '°F', 'current_temp': 100, 'preset_temp': 103, 'error_code': False}
The bolded section is different, between the preset temperature and current temperature. I tried finding the intex protocol but I'm guessing it is by trial and error. I'm not sure what is correcting the issue - it may be a scheduled event from the app that causes the temperature to update and a different checksum?
The only thing I found about 0 and FF checksums was in relation to UDP pstavirs/ostinato#160
I'm not sure how to work around this as I understand that every command issues a status to begin with? I have tried executing an async_update_info to see if that changes anything.
Initializing IntexSpa instance...
IntexSpa instance initialized
'status' intent: Handling new intent...
'status' intent: a spa query is needed
'status' intent: new spa query...
Not connected to the spa, trying to connect...
Opening TCP connection with the spa at SPA_DEVICE:8990 with asyncio...
TCP connection established with the spa
Sending bytes to the spa: b'{"data": "8888060FEE0F01DA", "sid": "16877683219221", "type": 1}'
Receiving bytes from the spa: b'{"sid":"16877683219221","data":"FFFF110F0107006400000086808086680000FC","result":"ok","type":2}\n'
'status' intent: new status is rendered
'status' intent: Handling new intent...
'status' intent: a spa query is needed
'status' intent: new spa query...
Sending bytes to the spa: b'{"data": "8888060FEE0F01DA", "sid": "16877683220962", "type": 1}'
Receiving bytes from the spa: b'{"sid":"16877683220962","data":"FFFF110F0107006400000086808086680000FC","result":"ok","type":2}\n'
'status' intent: new status is rendered
'preset_temp' intent: Handling new intent...
'preset_temp' intent: triggering a preliminary 'update' intent...
'status' intent: Handling new intent...
'status' intent: a spa query is needed
'status' intent: new spa query...
Sending bytes to the spa: b'{"data": "8888060FEE0F01DA", "sid": "16877683221893", "type": 1}'
Receiving bytes from the spa: b'{"sid":"16877683221893","data":"FFFF110F0107006400000086808086680000FC","result":"ok","type":2}\n'
'status' intent: new status is rendered
'preset_temp' intent: a spa query is needed
'preset_temp' intent: new spa query...
Sending bytes to the spa: b'{"data": "8888050F0C6767", "sid": "16877683222765", "type": 1}'
Receiving bytes from the spa: b'{"sid":"16877683222765","data":"FFFF110F0107006400000085808085670000FF","result":"ok","type":2}\n'
Malformed spa response during spa querying
'preset_temp' intent: new spa query...
Sending bytes to the spa: b'{"data": "8888050F0C6767", "sid": "16877683247193", "type": 1}'
Receiving bytes from the spa: b'{"sid":"16877683247193","data":"FFFF110F0107006400000085808085670000FF","result":"ok","type":2}\n'
Malformed spa response during spa querying
'preset_temp' intent: new spa query...
Sending bytes to the spa: b'{"data": "8888050F0C6767", "sid": "16877683270894", "type": 1}'
Receiving bytes from the spa: b'{"sid":"16877683270894","data":"FFFF110F0107006400000085808085670000FF","result":"ok","type":2}\n'
Malformed spa response during spa querying
Spa is unreachable
Following #1
Feature request: Identify spa model and/or features from info response
Awesome work @mathieu-mp !
Compared to intex app which if flaky and moody, this is straight forward and works without a hitch.
I was wondering if there is any chance to implement MQTT support as this would make this solution even more universal.
This string is sent by android app during pairing
{"data":"","sid":"1654467840319","type":3}
SPA responds with
{"sid":"1654467840319","data":"{\"ip\":\"192.168.x.x\",\"uid\":\"0K040210272020102000008062\",\"dtype\":\"spa\"}","result":"ok","type":3}
We found out here
https://community.home-assistant.io/t/intex-pure-spa-wifi-control/323591/25
that last four numbers in uid are probably device model - my app says i have Bubble SPA V28062
Could you please implement this query in your lib?
Thanks
I tried looking to see if this is documented elsewhere, but didn't find it.
With no heating, it seems the current temperature is correctly reported with the status message, at least it changes over time.
However, when the heater is on it doesn't report updated temperatures. I noticed this on the intex app and had to force refresh it to give me an updated temperature. Today after leaving it heating overnight it still had the same temperature. The one way I found to force it to update was by changing the preset_temperature. It seemed that I had to set a different temperature for it to actually update the current temperature.
Am I missing something? I tried the three example get status scripts and info. before trying to change the temperature. Maybe it's my model (SC-WF10)? Anyway, it seems to be working now with making the call to change the preset temperature.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.