diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..f9c755e0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,207 @@ +# AGENTS.md + +Guide for coding agents working on this repository. +Read this before you change code. It covers what you cannot see from the code alone. + +## 1. What this is + +A custom Home Assistant integration for Gree air conditioners. Domain `gree_custom`, folder `custom_components/gree_custom/`. It talks to devices over local UDP, and can also use the Gree Cloud over MQTT. + +The domain is `gree_custom` and not `gree` because Home Assistant ships its own `gree` integration. Do not rename it. + +Older releases (4.x) used the domain `gree` and a different code base. Their config entries are flat, one device per entry. This version uses one hub entry per Gree account with a `devices` map and local-only hub entry when CLoud is not used. The two are not compatible and there is no automatic migration. Users moving from 4.x set the integration up again. + +## 2. Versions + +- Python: **3.14**. `.ruff.toml`, `mypy.ini` and `.pylintrc` all target 3.14. +- Home Assistant: the runtime this is tested on is 2026.3 or newer. HA 2026.3 was the first release that requires Python 3.14. +- Python 3.14 syntax is used on purpose. Example: `except GreeConnectionError, GreeProtocolError:` without brackets is valid (PEP 758). Do not "fix" it. +- `hacs.json` states the lowest HA version for HACS installs. Keep it in line with the Python version the code needs. HA 2026.1 and 2026.2 still run Python 3.13. + +## 3. Where things live + +Everything is under `custom_components/gree_custom/`. + +Protocol layer is in `aiogree`, no Home Assistant imports: + +| File | What it does | +| ------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `aiogree/api.py` | Wire protocol. `GreeProp` and `InfoProp` enums, pack builders, status and command requests, discovery. | +| `aiogree/cipher.py` | `CipherV1` (AES-ECB) and `CipherV2` (AES-GCM). `EncryptionVersion` enum. | +| `aiogree/transport_udp.py` | Local UDP transport. Port 7000. Reuses one socket. | +| `aiogree/transport_mqtt.py` | Gree Cloud MQTT transport, one broker per region. | +| `aiogree/cloud_api.py` | Gree Cloud REST login and device list, one host per region. | +| `aiogree/device.py` | `GreeDevice`. Binds, fetches info and status, pushes changes. | +| `aiogree/device_api_client.py` | Holds the bound session: transport, key, controller MAC. | +| `aiogree/device_state.py` | `DeviceState`. Raw values, pending values, which props are polled. | +| `aiogree/helpers.py` | Protocol layer helpers: Encrypt and decrypt packs, temperature math, `redact_str`... | +| `aiogree/const.py` | Protocol layer constants. | +| `aiogree/errors.py` | Protocol layer defined exceptions. | + +Home Assistant layer: + +| File | What it does | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| `__init__.py` | Entry setup. Builds transports and devices, starts one coordinator per device. | +| `coordinator.py` | `GreeCoordinator`. Polls on `scan_interval` and listens for pushed status. | +| `config_flow.py` | Setup and reconfigure flows. Local discovery, cloud login, per device options. | +| `climate.py`, `switch.py`, `sensor.py`, `binary_sensor.py`, `number.py`, `select.py` | Entity platforms. | +| `entity.py`, `platform_helpers.py` | Base entity and shared helpers. | +| `services.py`, `services.yaml` | Services `get_prop_values` and `get_prop_values_all`. | +| `diagnostics.py` | Diagnostics download. Redacts keys and passwords. | +| `const.py` | Config keys, defaults, mode maps. `CURRENT_CONF_VERSION`. | + +Also in the repo root: `supported-devices.md`, `manual-configuration.yaml`, `hacs.json`. + +## 4. How a device comes to life + +`GreeDevice.bind_with_transport()` does this, in order: + +1. Bind. Try the local transport first, then MQTT. +2. `fetch_device_info()`. Asks for the `InfoProp` columns. +3. `fetch_device_status()`. Asks for all polled `GreeProp` columns. +4. `_remove_unsupported_props()`. Any prop the device did not return is removed from polling for the life of the object. + +After that the coordinator calls `fetch_device_status()` every `scan_interval` seconds (default 60). + +State lives in `DeviceState`: + +- `raw` is what the device last reported. +- `pending` is what we want to send. `set()` writes here. +- `push_device_status()` sends pending values and refreshes raw. +- `supports(prop)` is true only if the prop is in `raw` and in the capability list. + +Entities are only created for props the device supports. Fewer entities than an older version gave is normal. + +## 5. Protocol facts that are easy to get wrong + +Encryption: + +- V1 is AES-128 ECB. Bind uses the generic key `GREE_GENERIC_DEVICE_KEY_ECB`. The device answers with its own key. All later packs use that key. +- V2 is AES-128 GCM with a fixed IV (`GCM_IV`) and a tag field in the packet. +- `EncryptionVersion.V1 = 1`, `V2 = 2`. In config, `"0"` means auto detect. +- A device silently drops any pack it cannot decrypt. No reply is the normal sign of a wrong key. + +Requests: + +- Packs are split into batches so no request passes `MAX_PACK_SIZE` (512 bytes) before encryption. See `api.py`, the status request builder. +- A device may return fewer columns than asked. That is normal. Missing columns are reported in the result. It is not possible to reliable associate columns and values. +- A device may return `r=200` with empty `cols` and `dat`. Treat this as "no data", not as "nothing is supported". At least one device does this for a request of 30 props that is only 347 bytes. +- The device does not type its values. `InfoProp` values can come back as `int` (seen: `ModelType` as `32768`) while others are `str`. Coerce everything to string on status pack received. + +Temperature: + +- `SetTem` is a whole number. `TemRec` adds the half degree. +- Some devices report sensors with a +40 offset. `TempOffsetResolver` in `aiogree/helpers.py` detects this from the values it sees. +- Fahrenheit uses the protocol's own lookup, not a formula. + +Discovery and network: + +- Local discovery is a UDP broadcast. `extra_scan_networks` and `extra_scan_hosts` exist for devices on another subnet where broadcast does not reach, and are probed via unicast. +- There are multiple ways to identify the devices (via their MAC addresses) + - MACs are formated by using lower case and no separators. + - There are 2 types of devices, normal units and VRF units. + - The communication to a device requires a device MAC (device to control) and a controller MAC (device responsible to manage the target device) + - For normal units both MACs are usually the same + - For VRF units, behavior changes depending on protocol + - Local (UDP): + - Device MAC: is a 12 chars MAC with 2 chars at the end, so not a normal MAC, obtained via discovery + - Controller MAC: is a 12 chars MAC + - Cloud (MQTT): + - Device MAC: is a 12 chars MAC with 2 chars at the end, so not a normal MAC, obtained via discovery + - Controller MAC: is the first 12 chars of Device MAC +- Encryption version 2 is only used locally, for MQTT use always v1 + +## 6. Config entry shape + +Version is `CURRENT_CONF_VERSION` in `const.py` (3 at the time of writing). One entry is a hub. A local only entry has `unique_id` `local_only`. + +Example, shortened: + +```json +{ + "cloud": null, + "devices": { + "": { + "connection": { + "local": { + "mac_controller_local": "", + "host": "192.168.x.x", + "port": 7000, + "timeout": 10, + "encryption_version": "1", + "max_online_attempts": 3 + }, + "cloud": { "prefer_cloud": false, "mac_controller_cloud": "" }, + "scan_interval": 60, + "disable_available_check": false, + "encryption_key": "", + "uid": 0 + }, + "options": { "name": "", "features": ["beeper"], "restore_states": true } + } + } +} +``` + +If you change this shape, bump `CURRENT_CONF_VERSION` in `const.py`. There is no migration code yet. Home Assistant will refuse to load older entries after a bump unless you add `async_migrate_entry` in `__init__.py`. + +## 7. Working on the code + +Use the devcontainer. `CONTRIBUTING.md` has the steps. Inside it, VS Code tasks do the work: + +- `Run Home Assistant`: starts HA with this component. Restart it after every code change. +- `Ruff: check`, `Ruff: format`, `Pylint`, `Mypy`: run all of them before a PR. The configs match HA core. +- `HA: Check config`, `HA: Compile translations`. + +There is no unit test suite. Testing means running Home Assistant against a device. If you do not have a device, say so in the PR. A fake device that speaks the UDP protocol is the best way to test failure paths without touching real hardware. + +Debug logs: + +```yaml +logger: + logs: + custom_components.gree_custom: debug +``` + +Inside HA the logger name is `custom_components.gree_custom.*`. If you import `aiogree` directly in a script, the logger name is different. + +## 8. Secrets in logs + +Never log an encryption key, cloud password or token in clear text. + +- Use `redact_str()` from `aiogree/helpers.py` for single values. +- Use `async_redact_data()` with `encryption_key` and `password` for dicts. `__init__.py`, `config_flow.py` and `diagnostics.py` already do this. Follow the same pattern. +- Do not add new `_LOGGER` lines that print packs before redaction. + +## 9. Translations + +- `translations/` holds files that ship. Only edit these when you change the flow or an entity name, and keep `en.json` complete. +- `translation-to-review/` holds files that are not checked yet. Do not move a file out of it unless a native speaker reviewed it. + +## 10. Versions and releases + +- The version lives in `manifest.json` and nowhere else. +- The maintainers own the version. Releases are cut with the workflows in `.github/workflows/`, which bump `manifest.json` and publish the release. Do not change the version in a normal PR unless a maintainer asks for it. +- Pre-release versions look like `4.0.0-alpha.105`. They are not tags or GitHub releases, so an exact pre-release cannot always be checked out again later. + +## 11. Writing rules + +These apply to everything you produce in this repo. + +- English for all output: code, comments, docstrings, docs, commit messages, PR titles and PR descriptions. +- Chat with the user can follow the user's own language. +- Plain and simple English. No vague or padded text. Say what it is and stop. +- Sentence structure at B2 level or lower. Word choice at B1 level or lower. Technical terms are fine when there is no simpler word. +- Do not use em dashes. +- Match the style of the code around you. Do not reformat lines you did not need to touch. +- Do not mention AI tools in commits, PRs or branch names. + +## 12. Before you open a PR + +- [ ] Ruff, Pylint and Mypy pass. +- [ ] Both encryption versions are covered by your reasoning if you touched keys, ciphers or transports. +- [ ] No new secret can reach a log line. +- [ ] You tested against a real device, or you said in the PR that you could not. +- [ ] `en.json` is complete if you changed the flow or entity names. +- [ ] Docs in the repo root are updated if behavior changed. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index a491b3b1..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,67 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Custom Home Assistant integration for controlling Gree-compatible air conditioners over the local network via UDP (port 7000). Distributed through HACS. Domain: `gree`, version 3.3.2. - -**Dependencies**: `pycryptodome` (AES encryption), `aiofiles` (async file I/O) - -## Development Notes - -- **No build system, test suite, or linting configuration exists.** There is no setup.py, pyproject.toml, pytest, flake8, or similar tooling. -- All code lives under `custom_components/gree/`. There are no other source directories. -- To test changes, copy `custom_components/gree/` into a Home Assistant installation's `custom_components/` directory and restart HA. - -## Architecture - -### Data Flow - -``` -HA UI action → Entity method → GreeClimate.SendStateToAc() - → AES encrypt → UDP packet to device:7000 → device response - → AES decrypt → update _acOptions dict → update HA entity state - -Polling: every 60s via async_update() → GreeGetValues() -``` - -### Key Files - -| File | Purpose | -|---|---| -| `__init__.py` | Integration setup, YAML config schema, platform forwarding (climate/switch/number/select/sensor) | -| `climate.py` | **Core file (912 lines)**. `GreeClimate(ClimateEntity)` — HVAC control, state polling, temperature handling, all AC commands | -| `gree_protocol.py` | UDP communication, AES encryption (v1=ECB, v2=GCM), device discovery, key negotiation, retry logic (8 attempts with backoff) | -| `config_flow.py` | UI config flow: discovery → encryption detection → device setup. Also handles options flow for runtime reconfiguration | -| `const.py` | Protocol constants, mode mappings (Gree protocol values ↔ HA values), config option keys | -| `helpers.py` | Temperature math: 0.5°C precision encoding (SetTem/TemRec), °F↔°C conversion, ±40°C sensor offset auto-detection (`TempOffsetResolver`) | -| `entity.py` | `GreeEntity` base class, `GreeEntityDescription` dataclass | -| `switch.py` | 12 toggle entities (x-fan, lights, health, sleep, power save, etc.) | -| `sensor.py` | Outside temperature and room humidity sensors | -| `number.py` | Target temperature step configuration entity | -| `select.py` | External temperature sensor selection entity | - -### Encryption Protocol - -Two encryption versions exist: -- **v1**: AES-128 ECB with generic key `a3K8Bx%2r8Y7#xDh` -- **v2**: AES-128 GCM with device-specific key, fixed IV and AAD - -Encryption version is auto-detected during setup. The device key is retrieved via a handshake in `GetDeviceKey()`/`GetDeviceKeyGCM()`. - -### Temperature Handling - -The AC uses integer `SetTem` plus a `TemRec` bit for 0.5°C precision. Some devices report sensor temps with a +40°C offset. `TempOffsetResolver` auto-detects which mode the device uses based on observed temperature history. Fahrenheit support uses custom conversion functions (not simple formulas) due to protocol quirks. - -### Device State - -`GreeClimate._acOptions` dict tracks 19+ device properties: `Pow`, `Mod`, `SetTem`, `WdSpd`, `Air`, `Blo`, `Health`, `SwhSlp`, `Lig`, `SwUpDn`, `SwingLfRig`, `Quiet`, `Tur`, `StHt`, `TemUn`, `HeatCoolType`, `TemRec`, `SvSt`, `SlpMod`, and optionally `AntiDirectBlow`, `LigSen`, `OutEnvTem`, `TemSen`, `Buzzer_ON_OFF`. - -### Configuration - -Two config methods: UI config flow (recommended, with auto-discovery) and YAML import. See `manual-configuration.yaml` for YAML reference. Options flow allows runtime changes to available modes and sensor offset. - -### VRF Support - -VRF (Variable Refrigerant Flow) sub-units are addressed via MAC format `subMAC@mainMAC` and discovered through `get_subunits_list()`. diff --git a/README.md b/README.md index 220e3507..b14c7ffa 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,15 @@ [![HACS](https://img.shields.io/badge/HACS-Default-orange.svg)](https://hacs.xyz) -[![Home Assistant](https://img.shields.io/badge/Compatible-Home_Assistant_2026.4+-blue.svg)](https://www.home-assistant.io) +[![Home Assistant](https://img.shields.io/badge/Compatible-Home_Assistant_2026.3+-blue.svg)](https://www.home-assistant.io) # HomeAssistant-GreeClimateComponent -Custom Gree integration for Home Assistant written in Python 3. +Gree integration for Home Assistant written in Python 3. -This integration connects directly to your HVAC devices via their IP address on the local network, unlike the official mobile app, which establishes a direct connection only during initial setup and subsequently operates through Gree’s servers. - -**This integration only supports the Gree UDP protocol. If you have a newer firmware/device that only communicates using the new MQTT protocol, this integration will not work.** - -> [!IMPORTANT] -> Due to the many issues being created revolving "TimeOut"/"Cannot connect" errors, I will be closing these. Feel free to make a PR fixing your TimeOut/Cannot connect error. -> -> More information on the "why" can be found here: https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/issues/405#issuecomment-4300110823 +The integration supports controlling Gree devices locally (preferred) and through the Gree cloud. +You can read more about that [below](#connection-methods-and-configuration). For a comprehensive list of tested devices, see [Supported Devices](supported-devices.md). - -The integration attempts to obtain the encryption key through the initial setup protocol, which has been reverse-engineered. - -> [!WARNING] -> If your HVAC device was previously set up for remote access using a mobile app, the integration may fail to retrieve the encryption key automatically. Find out more about methods of obtaining your device key below. - +Feel free to open a new issue reporting working or non working devices. **If you are experiencing issues, please read the [Debugging](#debugging) section.** @@ -53,10 +42,12 @@ The integration can be added from the Home Assistant UI. 1. Navigate to **Settings** > **Devices & Services** and click **Add Integration**. 2. Search for **Gree Climate** -3. Choose automatic discovery or manual setup and fill in the desired `name`, `host`, and `MAC address`. -4. After a successful connection with the device, you will be asked to configure the device options. +3. Choose Cloud and/or Local setup and fill in the requested details. See [more](#connection-methods-and-configuration) on how these work +4. After a successful discovery, you will be asked which devices to add. +5. Select the devices and iterate through their configurations. -You can also **Reconfigure** a device by changing its options. Saving any changes in the options dialog automatically reloads the integration, so new settings take effect immediately without restarting Home Assistant. +You can also **Reconfigure** a entry by changing its options. Saving any changes in the options dialog automatically reloads the integration, so new settings take effect immediately without restarting Home Assistant. +While reconfiguring, devices not selected will be removed from the entry. ### Manual - YAML Configuration @@ -71,11 +62,51 @@ gree_custom: - device_name: "Gree AC" ``` -### Obtaining the Encryption Key +## Connection Methods and Configuration + +The integration supports both the local UDP protocol and the MQTT cloud protocol to communicate with the devices. It also supports enhancing local devices with cloud info during discovery. + +During configuration, you can select which of the methods are used to discover and connect to devices. + +**For better organization, multiple config entries are required!** +There is a config entry for all local-only devices and a config entries for each of the configured Gree Accounts. + +In a device page you can check which communication protocol is being used. + +### Local Configuration + +If you perform a local-only setup, the integration will automatically search for devices using the UDP protocol in the networks connected to your HA instance using a broadcast. +You can specify additional VLANs or hosts that will be scanned using a unicast. + +You will then see a list of discovered devices to select which ones to add to HA. This list excludes devices already configured. + +After successfully configuring the devices, they will be added to the _Local-only_ config entry. -The integration has the capability of automatically retrieve the encryption version and key of a device using the gree protocol, which has been reverse-engineered. +### Cloud Configuration -However, if your HVAC device was previously set up for remote access using a mobile app, the integration may fail to retrieve the encryption key automatically. +If you perform a cloud-only setup, the integration will retrieve the devices bound to your Gree account. + +You will then see a list of discovered devices to select which ones to add to HA. +If a configured local-only device that matches the cloud devices is found, it will be merged with the cloud-device and default to local control. + +After successfully configuring the devices, they will be added to a config entry exclusive to the cloud account. + +### Mixed Cloud + Local Configuration + +If you perform a setup using both methods, the integration will retrieve the devices bound to your Gree account and automatically discovered the local devices as in the [Local Configuration](#local-configuration). + +The list of discovered devices will be constrained to the cloud devices and you will see if they were also found locally. If a configured local-only device that matches the cloud devices is found, it will be merged with the cloud-device and default to local control. + +If a device responds to local commands, the cloud is only used to improve the discovered info (e.g., device name). This behaviour can be overriden in the Cloud Connection Settings of each device, where you can set it to use the MQTT connection instead. + +After successfully configuring the devices, they will be added to a config entry exclusive to the cloud account. + + +## Obtaining the Encryption Key + +The integration has the capability of automatically retrieve the encryption version and key of a device from the cloud account or using the Gree local protocol, which has been reverse-engineered. + +However, if your HVAC device was previously set up for remote access using a mobile app, the integration may fail to retrieve the encryption key automatically using the local protocol. #### Method 1: From Gree's cloud server @@ -98,9 +129,6 @@ sqlite3 data.ab 'select privateKey from db_device_20170503;' # but table name ca Optionally, you can also sniff the `uid` parameter. This is not needed for all devices. -### Icon configuration - -You can set custom icons for the climate entity by modifying the icon translation file `icons.json`. Refer to this documentation: https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/icon-translations/ ## Debugging @@ -159,7 +187,7 @@ Depending on the device configuration, specific Gree AC model, and firmware vers ### Configuration Controls - **Beeper**: Controls the beeper sounds from the air conditioner unit. When enabled, the unit will make sounds for button presses and status changes -- **Lights**: Controls the display lights on the air conditioner unit +- **Lights**: Controls the display lights on the air conditioner unit - **Auto Light**: Automatically controls the display lights based on HVAC operations. When enabled, lights will turn on/off with the AC unit. *Note: This is an integration feature, not an actual AC unit state* - **Light Sensor**: Enables or disables light sensor for automatic brightness. Requires lights to be enabled - **Auto X-Fan**: Automatically controls the X-Fan mode based on HVAC operations. When enabled, X-Fan will automatically turn on in cooling and dry modes. *Note: This is an integration feature, not an actual AC unit state* diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 9844c622..1afc7978 100755 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -585,7 +585,7 @@ async def gree_get_response_pack( json_data: dict, cipher: CipherBase, transport: GreeBaseTransport, -) -> dict: +) -> dict[str, Any]: """Send a request to the device and return the decoded response pack. Args: @@ -1025,7 +1025,9 @@ async def gree_get_status( return StatusResult(prop_values=status, missing_props=missing) -def gree_process_status_pack(pack: dict, props: list[str] | None) -> StatusResult: +def gree_process_status_pack( + pack: dict[str, Any], props: list[str] | None +) -> StatusResult: """Process a status pack. Args: @@ -1043,8 +1045,9 @@ def gree_process_status_pack(pack: dict, props: list[str] | None) -> StatusResul # dat = best-effort values, possibly incomplete # alignment between them is not guaranteed globally - cols = pack.get("cols") - dat = pack.get("dat") + # Ensure the lists are parsed as strings + cols = [str(value) for value in pack.get("cols", [])] + dat = [str(value) for value in pack.get("dat", [])] if cols is None or dat is None: raise GreeProtocolError("No data received while getting device status") diff --git a/custom_components/gree_custom/aiogree/cloud_api.py b/custom_components/gree_custom/aiogree/cloud_api.py index f5e54702..29f3f0ec 100755 --- a/custom_components/gree_custom/aiogree/cloud_api.py +++ b/custom_components/gree_custom/aiogree/cloud_api.py @@ -102,11 +102,11 @@ class GreeRegion(StrEnum): AS = "East South Asia" EU = "Europe" IN = "India" - LA = "Latin American" + LA = "Latin America" ME = "Middle East" - US = "North American" + US = "North America" RU = "Russia" - SA = "South American" + SA = "South America" CLOUD_SERVERS = { diff --git a/custom_components/gree_custom/aiogree/device.py b/custom_components/gree_custom/aiogree/device.py index 95e4765b..af38df01 100755 --- a/custom_components/gree_custom/aiogree/device.py +++ b/custom_components/gree_custom/aiogree/device.py @@ -16,6 +16,7 @@ InfoProp, OperationMode, SleepMode, + StatusResult, TemperatureUnits, VerticalSwingMode, extract_fw_version, @@ -170,6 +171,8 @@ async def bind_with_transport( exc_info=True, ) else: + self._client.add_status_listener(self._device_pushed_status) + # Fetch initial information after sucessful bind await self.fetch_device_info() await self.fetch_device_status() @@ -178,6 +181,10 @@ async def bind_with_transport( raise error + def _device_pushed_status(self, status: dict[str, str]) -> None: + _LOGGER.debug("[%s] Got data pushed from the device", self.unique_id) + self._state.process_new_state(status) + async def unbind_device(self) -> None: """Properly disconnect the device from transport.""" if not self._client.bound: @@ -204,7 +211,7 @@ async def fetch_device_info(self) -> None: try: props = [prop.value for prop in InfoProp] - raw_info, _ = await self._client.query_props(props, len(props)) + result = await self._client.query_props(props, len(props)) except GreeConnectionError, GreeProtocolError: _LOGGER.exception( @@ -226,10 +233,13 @@ async def fetch_device_info(self) -> None: else: _LOGGER.debug( - "[%s:%s] Got device info: %s", self.unique_id, self.transport, raw_info + "[%s:%s] Got device info: %s", + self.unique_id, + self.transport, + result.prop_values, ) - self._state.process_new_state(raw_info) + self._state.process_new_state(result.prop_values) _LOGGER.debug(self._state.info) @@ -250,16 +260,19 @@ async def fetch_device_status(self) -> None: ) try: - status, _ = await self._client.query_props( + result = await self._client.query_props( [prop.value for prop in self._state.polled_properties], len(self._state.polled_properties), ) _LOGGER.debug( - "[%s:%s] Got device status: %s", self.unique_id, self.transport, status + "[%s:%s] Got device status: %s", + self.unique_id, + self.transport, + result.prop_values, ) - self._state.process_new_state(status) + self._state.process_new_state(result.prop_values) except GreeConnectionError, GreeProtocolError: _LOGGER.exception( @@ -385,18 +398,19 @@ def gather_diagnostics(self) -> dict[str, Any]: data["state_info"] = dict(self._state.info) data["state"] = {str(k): v for k, v in self._state.raw.items()} data["state_pending"] = {str(k): v for k, v in self._state.pending.items()} + data["state_unknown"] = {str(k): v for k, v in self._state.unknown.items()} return data async def query_props( self, props: list[str], request_batch: int = 1, error_as_missing: bool = False - ) -> tuple[dict[str, str], list[str]]: + ) -> StatusResult: """Query the value of the given props.""" return await self._client.query_props(props, request_batch, error_as_missing) async def query_props_all( self, request_batch: int = 1, error_as_missing: bool = False - ) -> tuple[dict[str, str], list[str]]: + ) -> StatusResult: """Query all possible props.""" return await self._client.query_all_props(request_batch, error_as_missing) diff --git a/custom_components/gree_custom/aiogree/device_api_client.py b/custom_components/gree_custom/aiogree/device_api_client.py index f8ad5673..5576a674 100755 --- a/custom_components/gree_custom/aiogree/device_api_client.py +++ b/custom_components/gree_custom/aiogree/device_api_client.py @@ -8,6 +8,7 @@ GreeProp, InfoProp, OtherProps, + StatusResult, gree_get_status, gree_process_status_pack, gree_set_status, @@ -123,6 +124,8 @@ async def unbind(self) -> None: self.controller_mac, ) + self._listeners.clear() + self._bound = False self._available = False self._cipher = None @@ -160,7 +163,7 @@ async def query_props( props: list[str], request_batch: int = 1, error_as_missing: bool = False, - ) -> tuple[dict[str, str], list[str]]: + ) -> StatusResult: """Query the status value of device properties.""" if not self._bound: await self.rebind() @@ -196,13 +199,13 @@ async def query_props( self._available = True - return state, missing + return StatusResult(prop_values=state, missing_props=missing) async def query_all_props( self, request_batch: int = 1, error_as_missing: bool = False, - ) -> tuple[dict[str, str], list[str]]: + ) -> StatusResult: """Query all possible props.""" all_props = [ diff --git a/custom_components/gree_custom/aiogree/device_state.py b/custom_components/gree_custom/aiogree/device_state.py index 879c7e2e..f9f6a7e0 100755 --- a/custom_components/gree_custom/aiogree/device_state.py +++ b/custom_components/gree_custom/aiogree/device_state.py @@ -19,6 +19,7 @@ def __init__(self, device_id: str, capabilities: Iterable[GreeProp]) -> None: self._raw: dict[GreeProp, int] = {} self._pending: dict[GreeProp, int] = {} self._info: dict[InfoProp, str] = {} + self._unknown: dict[str, str] = {} self._capabilities = set(capabilities) @@ -104,6 +105,7 @@ def process_new_state(self, new_state: dict[str, str]) -> None: self._info[INFOPROP_KEY_TO_ENUM[key]] = value else: + self._unknown[key] = value unknown.append(key) except ValueError, TypeError: @@ -200,3 +202,8 @@ def pending(self) -> MappingProxyType[GreeProp, int]: def info(self) -> MappingProxyType[InfoProp, str]: """The Device Info property values.""" return MappingProxyType(self._info) + + @property + def unknown(self) -> MappingProxyType[str, str]: + """The unknown property values.""" + return MappingProxyType(self._unknown) diff --git a/custom_components/gree_custom/config_flow.py b/custom_components/gree_custom/config_flow.py index 65edd062..4b6cc18d 100755 --- a/custom_components/gree_custom/config_flow.py +++ b/custom_components/gree_custom/config_flow.py @@ -3,10 +3,17 @@ from collections.abc import Mapping from ipaddress import IPv4Address, IPv4Network, ip_address, ip_network import logging -from typing import Any, override +from typing import TYPE_CHECKING, Any, override from aiomqtt import MqttError -import voluptuous as vol + +if TYPE_CHECKING: + import probatio +else: + try: + import probatio + except ImportError: + import voluptuous as probatio from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass @@ -138,9 +145,9 @@ _LOGGER = logging.getLogger(__name__) -SETUP_SCHEMA = vol.Schema( +SETUP_SCHEMA = probatio.Schema( { - vol.Required(CONF_DISCOVERY, default=["cloud", "local"]): SelectSelector( + probatio.Required(CONF_DISCOVERY, default=["cloud", "local"]): SelectSelector( SelectSelectorConfig( options=["cloud", "local"], multiple=True, @@ -151,20 +158,20 @@ ) -def _setup_cloud_schema(defaults_values: dict | None = None) -> vol.Schema: +def _setup_cloud_schema(defaults_values: dict | None = None) -> probatio.Schema: defaults = defaults_values or {} - return vol.Schema( + return probatio.Schema( { - vol.Required( + probatio.Required( CONF_EMAIL, default=defaults.get(CONF_EMAIL, ""), ): str, - vol.Required( + probatio.Required( CONF_PASSWORD, default=defaults.get(CONF_PASSWORD, ""), ): str, - vol.Required( + probatio.Required( CONF_REGION, default=defaults.get(CONF_REGION), ): SelectSelector( @@ -177,18 +184,18 @@ def _setup_cloud_schema(defaults_values: dict | None = None) -> vol.Schema: ) -def _setup_local_schema(default_values: dict | None = None) -> vol.Schema: +def _setup_local_schema(default_values: dict | None = None) -> probatio.Schema: defaults = default_values or {} - return vol.Schema( + return probatio.Schema( { - vol.Optional( + probatio.Optional( CONF_EXTRA_SCAN_NETWORKS, description={ "suggested_value": defaults.get(CONF_EXTRA_SCAN_NETWORKS, []) }, ): TextSelector(TextSelectorConfig(multiple=True, multiline=False)), - vol.Optional( + probatio.Optional( CONF_EXTRA_SCAN_HOSTS, description={ "suggested_value": defaults.get(CONF_EXTRA_SCAN_HOSTS, []) @@ -200,10 +207,10 @@ def _setup_local_schema(default_values: dict | None = None) -> vol.Schema: def _setup_picker_schema( default: list[str], options: dict[str, GreeDiscoveredDevice] -) -> vol.Schema: - return vol.Schema( +) -> probatio.Schema: + return probatio.Schema( { - vol.Required(CONF_DEVICES, default=default): SelectSelector( + probatio.Required(CONF_DEVICES, default=default): SelectSelector( SelectSelectorConfig( options=[ SelectOptionDict(value=m, label=d.friendly_name) @@ -218,25 +225,27 @@ def _setup_picker_schema( def _setup_device_connection_options_schema( device_info: GreeDiscoveredDevice, default_values: dict | None = None -) -> vol.Schema: +) -> probatio.Schema: defaults: dict = default_values or {} defaults_local = defaults.get(CONF_DEVICE_CONNECTION_LOCAL, {}) defaults_cloud = defaults.get(CONF_DEVICE_CONNECTION_CLOUD, {}) - return vol.Schema( + return probatio.Schema( { - vol.Required( + probatio.Required( CONF_SCAN_INTERVAL, default=defaults.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL), - ): vol.All(vol.Coerce(int), vol.Range(min=MIN_SCAN_INTERVAL)), - vol.Required( + ): probatio.All( + probatio.Coerce(int), probatio.Range(min=MIN_SCAN_INTERVAL) + ), + probatio.Required( CONF_DISABLE_AVAILABLE_CHECK, default=defaults.get( CONF_DISABLE_AVAILABLE_CHECK, DEFAULT_DISABLE_AVAILABLE_CHECK, ), ): cv.boolean, - vol.Optional( + probatio.Optional( CONF_ENCRYPTION_KEY, default=( defaults.get(CONF_ENCRYPTION_KEY) @@ -244,27 +253,27 @@ def _setup_device_connection_options_schema( or DEFAULT_ENCRYPTION_KEY ), ): TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD)), - vol.Required( + probatio.Required( CONF_UID, default=defaults.get(CONF_UID, device_info.user_id), ): cv.positive_int, - vol.Required(CONF_DEVICE_CONNECTION_LOCAL): section( - vol.Schema( + probatio.Required(CONF_DEVICE_CONNECTION_LOCAL): section( + probatio.Schema( { - vol.Optional( + probatio.Optional( CONF_MAC_CONTROLLER_LOCAL, default=( defaults_local.get(CONF_MAC_CONTROLLER_LOCAL) or device_info.mac_controller_local ), ): str, - vol.Optional( + probatio.Optional( CONF_HOST, default=( defaults_local.get(CONF_HOST) or device_info.host or "" ), ): str, - vol.Optional( + probatio.Optional( CONF_PORT, default=( defaults_local.get(CONF_PORT) @@ -272,13 +281,13 @@ def _setup_device_connection_options_schema( or DEFAULT_DEVICE_PORT ), ): cv.port, - vol.Required( + probatio.Required( CONF_TIMEOUT, default=defaults_local.get( CONF_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT ), ): cv.positive_int, - vol.Required( + probatio.Required( CONF_ENCRYPTION_VERSION, default=defaults_local.get( CONF_ENCRYPTION_VERSION, DEFAULT_ENCRYPTION_VERSION @@ -296,7 +305,7 @@ def _setup_device_connection_options_schema( mode=SelectSelectorMode.DROPDOWN, ) ), - vol.Required( + probatio.Required( CONF_MAX_ONLINE_ATTEMPTS, default=defaults_local.get( CONF_MAX_ONLINE_ATTEMPTS, @@ -306,17 +315,17 @@ def _setup_device_connection_options_schema( } ) ), - vol.Required(CONF_DEVICE_CONNECTION_CLOUD): section( - vol.Schema( + probatio.Required(CONF_DEVICE_CONNECTION_CLOUD): section( + probatio.Schema( { - vol.Required( + probatio.Required( CONF_PREFER_CLOUD, default=defaults_cloud.get( CONF_PREFER_CLOUD, DEFAULT_PREFER_CLOUD, ), ): cv.boolean, - vol.Optional( + probatio.Optional( CONF_MAC_CONTROLLER_CLOUD, default=defaults_cloud.get(CONF_MAC_CONTROLLER_CLOUD) or device_info.mac_controller_mqtt, @@ -330,13 +339,13 @@ def _setup_device_connection_options_schema( def _setup_device_options_schema( # noqa: C901 hass: HomeAssistant, device: GreeDevice, default_values: Mapping | None -) -> vol.Schema: +) -> probatio.Schema: defaults = default_values or {} schema: dict = {} schema.update( { - vol.Required( + probatio.Required( CONF_NAME, default=defaults.get(CONF_NAME, device.name), ): str @@ -346,7 +355,7 @@ def _setup_device_options_schema( # noqa: C901 if device.supports_property(GreeProp.OP_MODE): schema.update( { - vol.Optional( + probatio.Optional( CONF_HVAC_MODES, default=defaults.get(CONF_HVAC_MODES, DEFAULT_HVAC_MODES), ): SelectSelector( @@ -364,7 +373,7 @@ def _setup_device_options_schema( # noqa: C901 GreeProp.FEAT_TURBO_MODE: [GATTR_FEAT_TURBO], GreeProp.FEAT_QUIET_MODE: [GATTR_FEAT_QUIET_MODE], } - valid_fan_modes = [] + valid_fan_modes: list[str] = [] for prop, modes in fan_mapping.items(): if device.supports_property(prop): valid_fan_modes.extend(modes) @@ -372,7 +381,7 @@ def _setup_device_options_schema( # noqa: C901 if valid_fan_modes: schema.update( { - vol.Optional( + probatio.Optional( CONF_FAN_MODES, default=defaults.get(CONF_FAN_MODES, valid_fan_modes), ): SelectSelector( @@ -388,7 +397,7 @@ def _setup_device_options_schema( # noqa: C901 if device.supports_property(GreeProp.SWING_VERTICAL): schema.update( { - vol.Optional( + probatio.Optional( CONF_SWING_MODES, default=defaults.get(CONF_SWING_MODES, DEFAULT_SWING_MODES), ): SelectSelector( @@ -404,7 +413,7 @@ def _setup_device_options_schema( # noqa: C901 if device.supports_property(GreeProp.SWING_HORIZONTAL): schema.update( { - vol.Optional( + probatio.Optional( CONF_SWING_HORIZONTAL_MODES, default=defaults.get( CONF_SWING_HORIZONTAL_MODES, DEFAULT_SWING_HORIZONTAL_MODES @@ -427,7 +436,7 @@ def _setup_device_options_schema( # noqa: C901 if valid_features: schema.update( { - vol.Optional( + probatio.Optional( CONF_FEATURES, default=defaults.get(CONF_FEATURES, valid_features), ): SelectSelector( @@ -443,7 +452,7 @@ def _setup_device_options_schema( # noqa: C901 if device.supports_property(GreeProp.TARGET_TEMPERATURE): schema.update( { - vol.Required( + probatio.Required( CONF_TEMPERATURE_STEP, default=defaults.get( CONF_TEMPERATURE_STEP, DEFAULT_TARGET_TEMP_STEP @@ -462,7 +471,7 @@ def _setup_device_options_schema( # noqa: C901 schema.update( { - vol.Optional( + probatio.Optional( ATTR_EXTERNAL_TEMPERATURE_SENSOR, description={ "suggested_value": defaults.get( @@ -484,7 +493,7 @@ def _setup_device_options_schema( # noqa: C901 ), ) ), - vol.Optional( + probatio.Optional( ATTR_EXTERNAL_HUMIDITY_SENSOR, description={ "suggested_value": defaults.get(ATTR_EXTERNAL_HUMIDITY_SENSOR, "") @@ -503,14 +512,14 @@ def _setup_device_options_schema( # noqa: C901 ), ) ), - vol.Required( + probatio.Required( CONF_RESTORE_STATES, default=defaults.get(CONF_RESTORE_STATES, DEFAULT_RESTORE_STATES), ): cv.boolean, } ) - return vol.Schema(schema) + return probatio.Schema(schema) class SetupConfigFlow(ConfigFlow, domain=DOMAIN): @@ -527,7 +536,7 @@ def __init__(self) -> None: self._extra_networks: list[str] = [] self._extra_hosts: list[str] = [] - self._config_data: dict = {} + self._config_data: dict[str, Any] = {} self._config_data["device_connections"] = {} self._config_data["device_options"] = {} self._cloud_api: GreeCloudApi | None = None @@ -546,6 +555,11 @@ def __init__(self) -> None: self._options_by_controller: dict[str, Any] = {} self._options_by_model: dict[str, Any] = {} + async def async_step_import(self, import_config: dict) -> ConfigFlowResult: + """Handle import from configuration.yaml.""" + # TODO: Implement YAML import + return self.async_abort(reason="not_implemented") + @override async def async_step_dhcp( self, discovery_info: DhcpServiceInfo @@ -673,9 +687,9 @@ async def async_step_reconfigure( return self.async_show_form( step_id="reconfigure", - data_schema=vol.Schema( + data_schema=probatio.Schema( { - vol.Required( + probatio.Required( "include_local", default=has_local, ): cv.boolean @@ -1182,9 +1196,11 @@ async def _async_finish(self) -> ConfigFlowResult: # noqa: C901 """Create or update the entry.""" if self.source == SOURCE_REAUTH: + cloud_conf: dict[str, str] = self._config_data.get(CONF_CLOUD, {}) return self.async_update_reload_and_abort( self._get_reauth_entry(), - data_updates={CONF_CLOUD: self._config_data.get(CONF_CLOUD, {})}, + title=f"Gree Account: {cloud_conf.get(CONF_UID)} ({cloud_conf.get(CONF_EMAIL)})", + data_updates={CONF_CLOUD: cloud_conf}, ) device_registry = dr.async_get(self.hass) @@ -1276,8 +1292,11 @@ async def _async_finish(self) -> ConfigFlowResult: # noqa: C901 None, ) - title = self._config_data.get(CONF_CLOUD, {}).get( - CONF_EMAIL, "Local-only Devices" + cloud_conf: dict[str, str] = self._config_data.get(CONF_CLOUD, {}) + title = ( + f"Gree Account: {cloud_conf.get(CONF_UID)} ({cloud_conf.get(CONF_EMAIL)})" + if cloud_conf.get(CONF_EMAIL) + else "Local-only Devices" ) if update_entry: diff --git a/custom_components/gree_custom/manifest.json b/custom_components/gree_custom/manifest.json index c51a875f..f39eff21 100755 --- a/custom_components/gree_custom/manifest.json +++ b/custom_components/gree_custom/manifest.json @@ -32,5 +32,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/issues", "requirements": ["asyncio_dgram", "aiomqtt", "pydantic"], - "version": "4.0.0-alpha.105" + "version": "5.0.0-alpha.106" } diff --git a/custom_components/gree_custom/services.py b/custom_components/gree_custom/services.py index e505aec7..3bde3e30 100755 --- a/custom_components/gree_custom/services.py +++ b/custom_components/gree_custom/services.py @@ -3,7 +3,13 @@ import logging from typing import TYPE_CHECKING, Any -import voluptuous as vol +if TYPE_CHECKING: + import probatio +else: + try: + import probatio + except ImportError: + import voluptuous as probatio from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID @@ -24,17 +30,17 @@ _LOGGER = logging.getLogger(__name__) SVC_BASE_SCHEMA = { - vol.Required(ATTR_DEVICE_ID): cv.string, + probatio.Required(ATTR_DEVICE_ID): cv.string, } SVC_GET_PROPS_ALL = "get_prop_values_all" -SVC_GET_PROPS_ALL_SCHEMA = vol.Schema(SVC_BASE_SCHEMA) +SVC_GET_PROPS_ALL_SCHEMA = probatio.Schema(SVC_BASE_SCHEMA) SVC_GET_PROPS = "get_prop_values" -SVC_GET_PROPS_SCHEMA = vol.Schema( +SVC_GET_PROPS_SCHEMA = probatio.Schema( SVC_GET_PROPS_ALL_SCHEMA.extend( { - vol.Required(ATTR_SVC_PROPS): vol.All([cv.string]), + probatio.Required(ATTR_SVC_PROPS): probatio.All([cv.string]), } ) ) @@ -109,11 +115,11 @@ async def async_get_prop_values_all(call: ServiceCall) -> ServiceResponse: _LOGGER.debug("Service called: get_prop_values_all") device: GreeDevice = async_get_device_from_service_call(call) - state, missing = await device.query_props_all(error_as_missing=True) + query_result = await device.query_props_all(error_as_missing=True) result: dict[str, Any] = {} - result["states"] = state - result["missing"] = missing + result["states"] = query_result.prop_values + result["missing"] = query_result.missing_props return result @@ -126,11 +132,11 @@ async def async_get_prop_values(call: ServiceCall) -> ServiceResponse: props = call.data[ATTR_SVC_PROPS] device: GreeDevice = async_get_device_from_service_call(call) - state, missing = await device.query_props(props=props, error_as_missing=True) + query_result = await device.query_props(props=props, error_as_missing=True) result: dict[str, Any] = {} - result["states"] = state - result["missing"] = missing + result["states"] = query_result.prop_values + result["missing"] = query_result.missing_props return result diff --git a/custom_components/gree_custom/translations/en.json b/custom_components/gree_custom/translations/en.json index d791d87c..a3835bb9 100644 --- a/custom_components/gree_custom/translations/en.json +++ b/custom_components/gree_custom/translations/en.json @@ -363,6 +363,12 @@ "message": "Smart Dry is only available in Cool mode." } }, + "issues": { + "device_connection_failed": { + "title": "Device connection failed", + "description": "Unable to connect to device {device}." + } + }, "services": { "get_prop_values_all": { "name": "Query all properties", diff --git a/custom_components/gree_custom/translations/pt.json b/custom_components/gree_custom/translations/pt.json index a75b13d2..015f91a3 100644 --- a/custom_components/gree_custom/translations/pt.json +++ b/custom_components/gree_custom/translations/pt.json @@ -371,6 +371,12 @@ } } }, + "issues": { + "device_connection_failed": { + "title": "Falha de comunicação", + "description": "Não foi possível comunicar com o dispositivo {device}." + } + }, "services": { "get_prop_values": { "name": "Consultar propriedades", diff --git a/hacs.json b/hacs.json index 76ed70f8..dfe4e96f 100644 --- a/hacs.json +++ b/hacs.json @@ -1,4 +1,4 @@ { - "name": "Gree A/C", - "homeassistant": "2026.1" -} \ No newline at end of file + "name": "Gree A/C", + "homeassistant": "2026.3" +} diff --git a/manual-configuration.yaml b/manual-configuration.yaml index 39a4755b..0e5da79b 100644 --- a/manual-configuration.yaml +++ b/manual-configuration.yaml @@ -1,5 +1,5 @@ # Gree Climate Integration - Manual Configuration Example -# +# # This file contains all possible configuration options for the Gree Climate integration # when using YAML configuration instead of the UI config flow. # @@ -8,94 +8,94 @@ # If an option is not provided the default values will be used. # For option lists, pass empty list ([]) to disable the option. # -# MAC address Format can be XX:XX:XX:XX:XX:XX, XX-XX-XX-XX-XX-XX, xxxxxxxxxxxx +# MAC address Format is lowercase without separators # -# For VRF units the MAC is usually xxxxxxxxxxxx@yyyyyyyyyyyy where -# the first part (x) is the device MAC and the second (y) the main device MAC which controls the device -# In this case it is preferred to place the main device MAC on the top config and -# the sub device MAC on the device list config - +# For VRF units the MACs are: +# MAC: xxxxxxxxxxxxyy <14 chars> +# MAC Controller Local: zzzzzzzzzzzz <12 chars> +# MAC Controller Cloud: xxxxxxxxxxxx <12 chars, first from MAC> +# +# For single units the MACs are: +# MAC: xxxxxxxxxxxx <12 chars> +# MAC Controller Local: xxxxxxxxxxxx <12 chars, same as MAC> +# MAC Controller Cloud: xxxxxxxxxxxx <12 chars, same as MAC> gree_custom: - - host: "192.168.1.100" # IP Address of AC | required | str - mac: "20-FA-BB-12-34-56" # MAC Address of Main AC Unit | required | str - advanced: - port: 7000 # Port number to connect to the device | int | default = 7000 - encryption_version: 2 # The encryption version to use with the device | options = "Auto-Detect", 1, 2 | default = "Auto-Detect" - encryption_key: "my_device_key" # Custom encryption key | str | default = - uid: 0 # Device identifier which is not needed for all devices, can be sniffed if required | positive int | default = 0 - disable_available_check: false # boolean | default = false - max_online_attempts: 3 # Number connection attempts made with device before it is marked as unavailable | positive int | default = 5 - timeout: 10 # Seconds before a connection attempt times out | positive int (seconds) | default = 10 - devices: # List of the devices that will be created (optional if only one device and no configuration required) - - device_name: "Gree AC" # Name for the AC unit | str | default = "Gree AC ]" - mac: "20-FA-BB-12-34-56" # MAC Address of the sub AC unit (same as Main device if not VRF) | required | str - hvac_modes: # Standard Home Assistant HVAC Modes to enable | list | options = ["auto", "cool", "dry", "fan_only", "heat", "off"] | default = all options - - "auto" - - "cool" - - "dry" - - "fan_only" - - "heat" - - "off" - fan_modes: # Supported fan modes | list | options = ["Auto", "Low", "MediumLow", "Medium", "MediumHigh", "High", "turbo", "quiet"] | default = all options - - "Auto" - - "Low" - - "MediumLow" - - "Medium" - - "MediumHigh" - - "High" - - "turbo" - - "quiet" - swing_modes: # Supported vertical swing modes | list | options = ["Default", "FullSwing", "FixedUpper", "FixedUpperMiddle", "FixedMiddle", "FixedLowerMiddle", "FixedLower", "SwingLower", "SwingLowerMiddle", "SwingMiddle", "SwingUpperMiddle", "SwingUpper"] | default = all options - - "Default" - - "FullSwing" - - "FixedUpper" - - "FixedUpperMiddle" - - "FixedMiddle" - - "FixedLowerMiddle" - - "FixedLower" - - "SwingLower" - - "SwingLowerMiddle" - - "SwingMiddle" - - "SwingUpperMiddle" - - "SwingUpper" - swing_horizontal_modes: # Supported horizontal swing modes | list | options = ["Default", "FullSwing", "Left", "LeftCenter", "Center", "RightCenter", "Right"] | default = all options - - "Default" - - "FullSwing" - - "Left" - - "LeftCenter" - - "Center" - - "RightCenter" - - "Right" - features: # Supported device features | list | options = ["beeper", "air", "xfan", "sleep", "eightdegheat", "lights", "health", "anti_direct_blow", "powersave", "light_sensor", "faults", "humidity_control"] | default = all options - - "beeper" - - "air" - - "xfan" - - "sleep" - - "eightdegheat" - - "lights" - - "health" - - "anti_direct_blow" - - "powersave" - - "light_sensor" - - "faults" - - "humidity_control" - target_temp_step: 1 # Number of degrees increase or decrease when changing the temperature | 0.5 < int < 5, 0.5 increments | default = 1 - external_temperature_sensor: "None" # Sets a given temperature sensor as the sensor for the AC | str (Entity ID) | default = "None" - external_humidity_sensor: "None" # Sets a given humidity sensor as the sensor for the AC | str (Entity ID) | default = "None" - restore_states: true # Wether to restore the last HA state to device when HA starts | bool | default = true - scan_interval: 60 # Device polling rate | int > 5 | default = 60 - - -# Example for multiple AC units: -# gree_custom: -# - host: "192.168.1.101" -# mac: "20-FA-BB-12-34-56" -# advanced: -# encryption_version: 2 - -# - host: "192.168.1.102" -# mac: "20-FA-BB-12-34-57" -# devices: -# - name: "Gree AC" -# mac: "20-FA-BB-12-34-57" + - cloud: # Info about a Gree cloud accont | optional + email: "user@server.com" # User email | required | str + password: "my-password" # User password | required | str + region: "Europe" # Gree account region | required | str | options = ["Australia", "China Mainland", "East South Asia", "Europe", "India", "Latin America", "Middle East" ,"North America", "Russia", "South America"] + devices: # List of configured gree devices | required + # Example of single unit + "20fabb123456": # MAC Address of the device | required | str + connection: + scan_interval: 60 # Device polling rate | int > 5 | default = 60 + disable_available_check: false # Change entity availability based on device connection | boolean | default = false + encryption_key: "my_device_key" # Custom encryption key | str | default = "" + uid: 0 # User identifier of device owner which is not needed for all devices, can be sniffed if required | positive int | default = 0 + local: # optional, if ommitted requires cloud + mac_controller_local: "20fabb123456" # MAC Address of the local controller (see above) | required | str + host: "192.168.1.100" # IP Address of AC | required | str + port: 7000 # Port number to connect to the device | int | default = 7000 + timeout: 10 # Seconds before a connection attempt times out | positive int (seconds) | default = 10 + encryption_version: # The encryption version to use with the device | options = "0", "1", "2" | default = "0" + max_online_attempts: 3 # Number connection attempts made with device before it is marked as unavailable | positive int | default = 3 + cloud: # optional, if ommitted requires local + prefer_cloud: false # When local is present, prefer to use cloud | optional | bool | default = false + mac_controller_cloud: "20fabb123456" # MAC Address of the local controller (see above) | required | str + options: + name: "Gree AC" # Name for the AC unit | required | str + hvac_modes: # Standard Home Assistant HVAC Modes to enable | list | options = ["auto", "cool", "dry", "fan_only", "heat", "off"] | default = all options + - "auto" + - "cool" + - "dry" + - "fan_only" + - "heat" + - "off" + fan_modes: # Supported fan modes | list | options = ["auto", "low", "medium_low", "medium", "medium_high", "high", "turbo", "quiet"] | default = all except "turbo" and "quiet" + - "auto" + - "low" + - "medium_low" + - "medium" + - "medium_high" + - "high" + - "turbo" + - "quiet" + swing_modes: # Supported vertical swing modes | list | options = ["default", "full_swing", "fixed_upper", "fixed_upper_middle", "fixed_middle", "fixed_lower_middle", "fixed_lower", "swing_lower", "swing_lower_middle", "swing_middle", "swing_upper_middle", "swing_upper"] | default = all options + - "default" + - "full_swing" + - "fixed_upper" + - "fixed_upper_middle" + - "fixed_middle" + - "fixed_lower_middle" + - "fixed_lower" + - "swing_lower" + - "swing_lower_middle" + - "swing_middle" + - "swing_upper_middle" + - "swing_upper" + swing_horizontal_modes: # Supported horizontal swing modes | list | options = ["default", "full_swing", "left", "left_center", "center", "right_center", "right"] | default = all options + - "default" + - "full_swing" + - "left" + - "left_center" + - "center" + - "right_center" + - "right" + features: # Supported device features | list | options = ["beeper", "air", "xfan", "sleep", "eightdegheat", "lights", "health", "anti_direct_blow", "powersave", "light_sensor", "faults", "humidity_control"] | default = all options + - "beeper" + - "air" + - "xfan" + - "sleep" + - "eightdegheat" + - "lights" + - "health" + - "anti_direct_blow" + - "powersave" + - "light_sensor" + - "faults" + - "humidity_control" + target_temp_step: 1 # Number of degrees increase or decrease when changing the temperature | 0.5 < int < 5, 0.5 increments | default = 1 + external_temperature_sensor: "None" # Sets a given temperature sensor as the sensor for the AC | str (Entity ID) | default = "None" + external_humidity_sensor: "None" # Sets a given humidity sensor as the sensor for the AC | str (Entity ID) | default = "None" + restore_states: true # Wether to restore the last HA state to device when HA starts | bool | default = true