├── hacs.json
├── .md.images
├── ha-plant-panel.png
├── ha-lovelace-plant-card.png
├── ha-lovelace-plant-card-maintenance.png
├── ha-lovelace-plant-card-info-maintenance.png
└── ha-lovelace-plant-card-sensors-maintenance.png
├── .gitignore
├── custom_components
└── huahuacaocao
│ ├── manifest.json
│ ├── __init__.py
│ ├── sensor.py
│ └── .plugin
│ └── hacs-card-for-xiaomi-mi-flora-and-flower-care.js
├── tsconfig.json
├── CHANGELOG.md
├── LICENSE
├── src
├── types.ts
├── style.ts
└── hacs-card-for-xiaomi-mi-flora-and-flower-care.ts
├── package.json
├── info.md
└── README.md
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Xiaomi mi flora and Flower Care Smart Monitor",
3 | "domains": ["sensor"]
4 | }
--------------------------------------------------------------------------------
/.md.images/ha-plant-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r-renato/hass-xiaomi-mi-flora-and-flower-care/HEAD/.md.images/ha-plant-panel.png
--------------------------------------------------------------------------------
/.md.images/ha-lovelace-plant-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r-renato/hass-xiaomi-mi-flora-and-flower-care/HEAD/.md.images/ha-lovelace-plant-card.png
--------------------------------------------------------------------------------
/.md.images/ha-lovelace-plant-card-maintenance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r-renato/hass-xiaomi-mi-flora-and-flower-care/HEAD/.md.images/ha-lovelace-plant-card-maintenance.png
--------------------------------------------------------------------------------
/.md.images/ha-lovelace-plant-card-info-maintenance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r-renato/hass-xiaomi-mi-flora-and-flower-care/HEAD/.md.images/ha-lovelace-plant-card-info-maintenance.png
--------------------------------------------------------------------------------
/.md.images/ha-lovelace-plant-card-sensors-maintenance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r-renato/hass-xiaomi-mi-flora-and-flower-care/HEAD/.md.images/ha-lovelace-plant-card-sensors-maintenance.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ####### ####### #######
2 | # Folders
3 |
4 | .idea
5 | __pycache__
6 | /node_modules/
7 | /.rpt2_cache/
8 |
9 | # ####### ####### #######
10 | # Files
11 | package-lock.json
12 |
13 |
--------------------------------------------------------------------------------
/custom_components/huahuacaocao/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "huahuacaocao",
3 | "name": "Xiaomi mi flora and Flower Care Smart Monitor",
4 | "documentation": "https://github.com/r-renato/hass-xiaomi-mi-flora-and-flower-care.git",
5 | "dependencies": [],
6 | "codeowners": ["@r-renato"],
7 | "requirements": []
8 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "lib": ["es2017", "dom", "dom.iterable"],
7 | "noEmit": true,
8 | "noUnusedParameters": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "strict": true,
12 | "noImplicitAny": false,
13 | "skipLibCheck": true,
14 | "resolveJsonModule": true,
15 | "experimentalDecorators": true,
16 | "strictNullChecks": false
17 | },
18 |
19 | "scripts": {
20 | "start": "rollup -c --watch",
21 | "build": "npm run lint && npm run rollup",
22 | "lint": "eslint src/*.ts",
23 | "rollup": "rollup -c"
24 | }
25 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # hass xiaomi mi flora and flower care change log
2 |
3 | The format is based on [Keep a Changelog](http://keepachangelog.com/)
4 | and this project adheres to [Semantic Versioning](http://semver.org/).
5 |
6 | ## [1.2.2] Unreleased
7 | ### Fixed
8 |
9 | ### Added
10 |
11 | ### Changed
12 |
13 | ## [1.2.1] 2020-05-13
14 | ### Fixed
15 | - Minor bug fix
16 |
17 | ## [1.2.0] 2020-05-13
18 | ### Changed
19 | - Sensor state red colored when the state is unknown
20 |
21 | ## [1.1.1] 2020-03-27
22 | ### Fixed
23 | - Minor bug fix
24 |
25 | ## [1.1.0]
26 | ### Fixed
27 | - minor bug fix
28 | ### Added
29 | - Add customizable display sessions
30 |
31 | ## [1.0.2] 2019-10-01
32 | ### Fixed
33 | - Custom component minor bug fix
34 |
35 | ## [1.0.1] 2019-09-28
36 | ### Fixed
37 | - Custom component minor bug fix
38 |
39 | ## [1.0.0] 2019-09-20
40 | - Initial stable version
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Renato Rossi, https://www.linkedin.com/in/renatorossi/
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { ActionConfig } from "custom-card-helpers";
2 |
3 | // TODO Add your configuration elements here for type-checking
4 | export interface CardConfig {
5 | type: string;
6 | name?: string;
7 | zone_name?: string;
8 | display?: string[] ;
9 | entity: string;
10 | }
11 | export interface FloraCarePlantRanges {
12 | "max_light_mmol": number,
13 | "min_light_mmol": number,
14 | "max_light_lux": number,
15 | "min_light_lux": number,
16 | "max_temp": number,
17 | "min_temp": number,
18 | "max_env_humid": number,
19 | "min_env_humid": number,
20 | "max_soil_moist": number,
21 | "min_soil_moist": number,
22 | "max_soil_ec": number,
23 | "min_soil_ec": number
24 | }
25 |
26 | export interface FloraCarePlant {
27 | attributes : {
28 | problem: string,
29 | sensors: [{ key: string }],
30 | ranges: FloraCarePlantRanges,
31 | maintenance: Object,
32 | info: Object,
33 | image: string
34 | } ;
35 | }
36 |
37 | export interface Sensor {
38 | state: string ;
39 | attributes : {
40 | friendly_name?: string ;
41 | icon?: string ;
42 | unit_of_measurement?: string ;
43 | }
44 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hass-xiaomi-mi-flora-and-flower-care",
3 | "version": "1.1.1",
4 | "description": "The Home Assistant custom component uses Flower Care™ Smart Monitor to retrieve flower information (http://www.huahuacaocao.com/product).",
5 | "keywords": [
6 | "home-assistant",
7 | "homeassistant",
8 | "hass",
9 | "automation",
10 | "lovelace",
11 | "custom-cards",
12 | "flora"
13 | ],
14 | "homepage": "https://github.com/r-renato/hass-xiaomi-mi-flora-and-flower-care#readme",
15 | "module": "hass-xiaomi-mi-flora-and-flower-care.js",
16 | "repository": {
17 | "type": "git",
18 | "url": "git+ssh://git@github.com/r-renato/hass-xiaomi-mi-flora-and-flower-care.git"
19 | },
20 | "author": "Renato Rossi",
21 | "license": "MIT",
22 | "dependencies": {
23 | "custom-card-helpers": "^1.0.8",
24 | "lit-element": "^2.1.0"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "^7.4.3",
28 | "@babel/plugin-proposal-class-properties": "^7.4.0",
29 | "@babel/plugin-proposal-decorators": "^7.4.0",
30 | "@typescript-eslint/eslint-plugin": "^1.4.2",
31 | "@typescript-eslint/parser": "^1.4.1",
32 | "eslint": "^5.14.1",
33 | "eslint-config-airbnb-base": "^13.1.0",
34 | "eslint-plugin-import": "^2.16.0",
35 | "prettier": "^1.16.4",
36 | "rollup": "^1.2.3",
37 | "rollup-plugin-babel": "^4.3.2",
38 | "rollup-plugin-node-resolve": "^4.0.1",
39 | "rollup-plugin-terser": "^4.0.4",
40 | "rollup-plugin-typescript2": "^0.19.2",
41 | "rollup-plugin-uglify": "^6.0.2",
42 | "rollup-plugin-execute": "^1.1.1",
43 | "typescript": "^3.3.3333"
44 | },
45 | "bugs": {
46 | "url": "https://github.com/r-renato/hass-xiaomi-mi-flora-and-flower-care/issues"
47 | },
48 | "main": "index.js",
49 | "scripts": {
50 | "start": "rollup -c --watch",
51 | "build": "npm run lint && npm run rollup",
52 | "lint": "eslint src/*.ts",
53 | "rollup": "rollup -c --no-compact "
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/style.ts:
--------------------------------------------------------------------------------
1 | import { css } from 'lit-element';
2 |
3 | let tooltipBGColor = css`rgba(50,50,50,0.85)`;
4 | let tooltipFGColor = css`#fff`;
5 | let tooltipBorderColor = css`rgb(14, 171, 56)`;
6 | let tooltipBorderWidth = 1;
7 | let tooltipCaretSize = 5;
8 | let tooltipWidth = 400;
9 | let tooltipLeftOffset = -325;
10 | let tooltipVisible = css`visible` ;
11 |
12 | const style = css`
13 | ha-card {
14 | padding: 24px 16px 16px 16px;
15 | background-repeat: no-repeat;
16 | background-size: 100% 100% !important;
17 | }
18 |
19 | .banner {
20 | display: flex;
21 | align-items: flex-end;
22 | background-repeat: no-repeat;
23 | background-size: cover;
24 | background-position: center;
25 | padding-top: 12px;
26 |
27 | background-color: var(--banner-background);
28 | border-radius: 3px;
29 | }
30 |
31 | .has-plant-image .banner {
32 | padding-top: 30%;
33 | }
34 |
35 | .header {
36 | @apply --paper-font-headline;
37 | line-height: 40px;
38 | padding: 8px 16px;
39 | font-weight: 500;
40 | font-size: 125%;
41 | }
42 |
43 | .has-plant-image .header {
44 | font-size: 16px;
45 | font-weight: 500;
46 | line-height: 16px;
47 | padding: 16px;
48 | color: white;
49 | width: 100%;
50 | background: rgba(0, 0, 0, var(--dark-secondary-opacity));
51 | }
52 |
53 | .content {
54 | display: flex;
55 | justify-content: space-between;
56 | padding: 16px 32px 24px 32px;
57 | background-color: var(--content-background);
58 | border-radius: 3px;
59 | }
60 |
61 | .has-plant-image .content {
62 | padding-bottom: 16px;
63 | }
64 |
65 | ha-icon {
66 | color: var(--paper-item-icon-color);
67 | margin-bottom: 8px;
68 | }
69 |
70 | .attributes {
71 | cursor: pointer;
72 | }
73 |
74 | .attributes div {
75 | text-align: center;
76 | }
77 |
78 | .problem {
79 | color: var(--google-red-500);
80 | font-weight: bold;
81 | }
82 |
83 | .uom {
84 | color: var(--secondary-text-color);
85 | }
86 |
87 | .table-tr-td-border-bottom {
88 | border-bottom-color: var(--table-tr-td-border-bottom);
89 | border-bottom: solid 1px;
90 | }
91 |
92 | .div-border-top {
93 | border-top-color: var(--table-tr-td-border-bottom);
94 | border-top: solid 1px;
95 | }
96 |
97 | .div-border-bottom >table {
98 | border-bottom-color: var(--table-tr-td-border-bottom);
99 | border-bottom: solid 1px;
100 | }
101 |
102 | /* CUSTOM */
103 |
104 | table {
105 | width: 100%;
106 | }
107 |
108 | table thead {
109 |
110 | }
111 |
112 | table tr {
113 | border-top: 1px solid black;
114 | }
115 |
116 | table tr:first-child {
117 | border-top: 0;
118 | }
119 |
120 | table tr:first-child {
121 | border-left: 0; border-right: 0;
122 | }
123 |
124 | table tr:last-child {
125 | border-bottom: 0;
126 | }
127 |
128 | table tr:last-child{
129 | border-right: 0;
130 | }
131 |
132 | .fcasttooltip {
133 | position: relative;
134 | display: inline-block;
135 | }
136 |
137 | .fcasttooltip .fcasttooltiptext {
138 | visibility: hidden;
139 |
140 | width: ${tooltipWidth}px;
141 | background-color: ${tooltipBGColor};
142 | color: ${tooltipFGColor};
143 | text-align: center;
144 | border-radius: 6px;
145 | border-style: solid;
146 | border-color: ${tooltipBorderColor};
147 | border-width: ${tooltipBorderWidth}px;
148 | padding: 5px 0;
149 | /* Position the tooltip */
150 | position: absolute;
151 | z-index: 1;
152 | top: 100%;
153 | left: 0%;
154 | margin-left: ${tooltipLeftOffset}px;
155 | }
156 |
157 | .fcasttooltip .fcasttooltiptext:after {
158 | content: "";
159 | position: absolute;
160 | top: 100%;
161 | left: 50%;
162 | margin-left: -${tooltipCaretSize}px;
163 | border-width: ${tooltipCaretSize}px;
164 | border-style: solid;
165 | border-color: ${tooltipBorderColor} transparent transparent transparent;
166 | }
167 |
168 | .fcasttooltip:hover .fcasttooltiptext {
169 | visibility: ${tooltipVisible};
170 | }
171 | `;
172 |
173 | export default style;
--------------------------------------------------------------------------------
/custom_components/huahuacaocao/__init__.py:
--------------------------------------------------------------------------------
1 | """Support for Xiaomi Flora devices using huahuacaocao.com"""
2 | import os
3 | from shutil import copyfile
4 | from datetime import timedelta
5 | import logging
6 |
7 | import voluptuous as vol
8 |
9 | import homeassistant.core as ha
10 | from homeassistant.const import (
11 | CONF_USERNAME,
12 | CONF_PASSWORD)
13 | from homeassistant.helpers import config_validation as cv
14 | from homeassistant.helpers import discovery
15 |
16 | import json
17 | import requests
18 |
19 | import random
20 | import socket
21 | import struct
22 |
23 | _LOGGER = logging.getLogger(__name__)
24 |
25 | COMPONENTS_WITH_CC_PLATFORM = [
26 | 'sensor',
27 | ]
28 |
29 | CONF_REGION = "region"
30 | DEFAULT_REGION = "CN"
31 |
32 | DOMAIN = "huahuacaocao"
33 |
34 | CONFIG_SCHEMA = vol.Schema(
35 | {
36 | DOMAIN: vol.Schema(
37 | {
38 | vol.Required(CONF_USERNAME): cv.string,
39 | vol.Required(CONF_PASSWORD): cv.string,
40 | vol.Optional(CONF_REGION, default=DEFAULT_REGION): cv.string,
41 | }
42 | )
43 | },
44 | extra=vol.ALLOW_EXTRA,
45 | )
46 |
47 | _HOSTNAME = 'eu-api.huahuacaocao.net'
48 | _ENDPOINT = 'https://' + _HOSTNAME + '/api/v2'
49 |
50 | DEFAULT_TIMEOUT = 10
51 |
52 | SCAN_INTERVAL = timedelta(seconds=60)
53 |
54 | SERVICE_API = 'flower_service'
55 |
56 |
57 | async def async_setup(hass, config):
58 | _LOGGER.info("__init__ async_setup start for domain %s.", DOMAIN)
59 |
60 | """Set up the environment."""
61 | if DOMAIN not in config:
62 | return True
63 |
64 | if DOMAIN not in hass.data:
65 | hass.data[DOMAIN] = {}
66 |
67 | config.setdefault(ha.DOMAIN, {})
68 | config.setdefault(DOMAIN, {})
69 |
70 | hass.data[DOMAIN][SERVICE_API] = ServiceAPI(config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], config[DOMAIN][CONF_REGION])
71 |
72 | # Set up platforms
73 | for component in COMPONENTS_WITH_CC_PLATFORM:
74 | _LOGGER.info("__init__ async_setup load_platform for component: '%s' in domain '%s'.", DOMAIN, component)
75 | hass.async_create_task(discovery.async_load_platform(hass, component, DOMAIN, {}, config))
76 |
77 | # Set up Lovelace Card
78 | fn_card = 'hacs-card-for-xiaomi-mi-flora-and-flower-care.js'
79 | path_module = os.path.dirname(os.path.abspath(__file__))
80 |
81 | path_source = os.path.join(path_module, '.plugin')
82 | path_target = os.path.join(path_module, '../../www/community/hacs-card-for-xiaomi-mi-flora-and-flower-care')
83 |
84 | os.makedirs(path_target, exist_ok=True)
85 |
86 | copyfile(path_source + '/' + fn_card, path_target + '/' + fn_card)
87 |
88 | _LOGGER.info("__init__ async_setup done for domain %s.", DOMAIN)
89 |
90 | return True
91 |
92 |
93 | async def async_setup_entry(hass, entry):
94 | """Set up a config entry.
95 |
96 | Parameters:
97 | argument1 (hass): Description of arg1
98 | argument2 (entry): Description of arg1
99 |
100 | Returns:
101 | int:Returning value
102 |
103 | """
104 | return await hass.data[DOMAIN].async_setup_entry(entry)
105 |
106 |
107 | async def async_unload_entry(hass, entry):
108 | """Unload a config entry."""
109 | return await hass.data[DOMAIN].async_unload_entry(entry)
110 |
111 |
112 | class ServiceAPI(object):
113 |
114 | def __init__(self, username, password, region):
115 | """Initialize the Service API"""
116 |
117 | ip = socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
118 |
119 | self._headers = {
120 | 'Content-Type': 'application/json; charset=utf-8',
121 | 'X-Hhcc-Region': region,
122 | 'X-Hhcc-Token': '',
123 | 'X-Real-Ip': ip
124 | }
125 |
126 | self._authorization_payload = {
127 | 'data': {
128 | 'email': username,
129 | 'password': password
130 | },
131 | 'extra': {
132 | 'app_channel': 'google',
133 | 'country': 'IT',
134 | 'lang': 'it',
135 | 'model': '',
136 | 'phone': 'samsung_SM-G955F_26',
137 | 'position': [],
138 | 'version': 'AS_3044_5.4.6',
139 | 'zone': 1
140 | },
141 | 'method': 'GET',
142 | 'path': '/token/email',
143 | 'service': 'auth'
144 | }
145 |
146 | self._retryLogin = True
147 | self._token = None
148 | # self.retrieve_authorization_token()
149 |
150 | @staticmethod
151 | def resolves_hostname(hostname):
152 | try:
153 | socket.gethostbyname(hostname)
154 | return True
155 | except socket.error:
156 | return False
157 |
158 | def get_authorization_token(self):
159 | if self._token is None:
160 | self.retrieve_authorization_token()
161 |
162 | return self._token
163 |
164 | def retrieve_authorization_token(self):
165 | """Retrieve authorizzation token for use huahuacaocao.com rest api
166 |
167 | Parameters:
168 | argument1 (hass): Description of arg1
169 | argument2 (entry): Description of arg1
170 |
171 | Returns:
172 | int:Returning value
173 |
174 | """
175 |
176 | if self._retryLogin:
177 | try:
178 | _LOGGER.debug("ServiceAPI retrieve_authorization_token headers: %s", self._headers)
179 | _LOGGER.debug("ServiceAPI retrieve_authorization_token payload: %s", self._authorization_payload)
180 |
181 | if not ServiceAPI.resolves_hostname( _HOSTNAME ):
182 | _LOGGER.error("Hostname (%s) could not be resolved.", _HOSTNAME )
183 |
184 | response = requests.request("POST", _ENDPOINT,
185 | json=self._authorization_payload, headers=self._headers,
186 | timeout=(10.05, 27), verify=False
187 | )
188 | _LOGGER.debug("ServiceAPI retrieve_authorization_token response data: %s", response.text)
189 |
190 | if response.status_code == 200:
191 | rdata = json.loads(response.text)
192 |
193 | if rdata['status'] == 100:
194 | self._token = rdata['data']['token']
195 | _LOGGER.debug("Token retrieved: %s", self._token)
196 |
197 | except socket.error as err:
198 | self._retryLogin = False
199 | _LOGGER.debug("Caught exception socket.error '%s' trying to retrieve access token.", err)
200 |
201 | def retrieve_flower_details(self, pid):
202 | import copy
203 |
204 | details_payload = {
205 | "data": {
206 | "lang": "en",
207 | "pid": pid
208 | },
209 | "extra": {
210 | "app_channel": "google",
211 | "country": "IT",
212 | "lang": "it",
213 | "model": "",
214 | "phone": "samsung_SM-G955F_26",
215 | "position": [],
216 | "version": "AS_3044_5.4.6",
217 | "zone": 1
218 | },
219 | "method": "GET",
220 | "path": "/plant/detail",
221 | "service": "pkb"
222 | }
223 |
224 | if not (self.get_authorization_token() is None):
225 | try:
226 | headers = copy.copy(self._headers)
227 | headers['X-Hhcc-Token'] = self.get_authorization_token()
228 |
229 | _LOGGER.debug("ServiceAPI retrieve_flower_details headers: %s", headers)
230 | _LOGGER.debug("ServiceAPI retrieve_flower_details payload: %s", details_payload)
231 |
232 | response = requests.request("POST", _ENDPOINT,
233 | json=details_payload, headers=headers,
234 | timeout=(10.05, 27), verify=False
235 | )
236 | _LOGGER.debug("ServiceAPI retrieve_flower_details response data: %s", response.text)
237 |
238 | result = None
239 |
240 | if response.status_code == 200:
241 | rdata = json.loads(response.text)
242 |
243 | if rdata['status'] == 100:
244 | result = rdata['data']
245 |
246 | return result
247 |
248 | except socket.error as err:
249 | self._retryLogin = False
250 | _LOGGER.debug("Caught exception socket.error '%s' trying to retrieve flower details.", err)
251 |
--------------------------------------------------------------------------------
/info.md:
--------------------------------------------------------------------------------
1 | # Xiaomi mi flora and Flower care integration
2 |
3 | The Home Assistant custom component uses Flower Care™ Smart Monitor to retrieve flower information (http://www.huahuacaocao.com/product).
4 | HuaHuaCaoCao means flowers & Plants in Chinese.
5 |
6 | [](https://github.com/custom-components/hacs)
7 |
8 | [![License][license-shield]](LICENSE)
9 | [](https://lgtm.com/projects/g/r-renato/hass-xiaomi-mi-flora-and-flower-care/alerts/)
10 | [](https://lgtm.com/projects/g/r-renato/hass-xiaomi-mi-flora-and-flower-care/context:python)
11 |
12 | [![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
13 |
14 | ## Please read carefully
15 |
16 | 1. Need to register to Flower Care™ Smart Monitor App
17 | on Google Android devices or
18 | on Apple iOS devices to use this component.
19 | 2. _This custom component extends the Home Assistant Plant monitor with plant information coming from the Flora Care application._
20 | 3. _The lovelace card is self installed by the component and can be used also without the xiaomi Flora sensor._
21 |
22 |
23 |
24 |
25 |
26 |
27 | ## Manual installation
28 |
29 | 1. Using the tool of choice, open the directory (folder) of your HA configuration (there you can find `configuration.yaml`).
30 | 2. If you do not have a `custom_components` directory (folder) there, you need to create it.
31 | 3. In the `custom_components` directory (folder) create a new folder called `huahuacaocao`.
32 | 4. Download _all_ the files from the `custom_components/huahuacaocao/` directory (folder) in this repository.
33 | 5. Place the files you downloaded in the new directory (folder) you created.
34 |
35 | ## Configuration
36 |
37 | 1. Install your preferred Flower Care™ Smart Monitor App
38 | 2. Register your credentials in App, the same credentials will be used to configure the `huahuacaocao` integration component
39 |
40 | ### Component variables
41 |
42 | **username**
43 | >(string)(Required)
The username to use with your Flower Care™ Smart Monitor App.
44 |
45 | **password**
46 | >(string)(Required)
The corresponding password in yourFlower Care™ Smart Monitor App.
47 |
48 | **region**
49 | >(string)(Optional)
Your country code (two-letter)
50 |
51 | #### Examples
52 |
53 | ```yaml
54 | huahuacaocao:
55 | username: !secret huahuacaocao_user
56 | password: !secret huahuacaocao_password
57 | region: EU
58 | ```
59 |
60 | ### Sensor variables
61 |
62 | **plant_id**
63 | >(string)(Required)
Plant alias. You can find it in the Plant Archive panel of the Flower Care™ Smart Monitor App
64 | >
65 | **Name**
66 | >(string)(Required)
Name to use in the frontend.
67 | >
68 | **sensors**
69 | >(list)(Required)
List of sensor measure entities.
70 | >
71 | >**moisture**
72 | >>(string)(Optional)
Moisture of the plant. Measured in %. Can have a min and max value set optionally.
73 | >
74 | >**battery**
75 | >>(string)(Optional)
Battery level of the plant sensor. Measured in %. Can only have a min level set optionally.
76 | >
77 | >**temperature**
78 | >>(string)(Optional)
Temperature of the plant. Measured in degrees Celsius. Can have a min and max value set optionally.
79 | >
80 | >**conductivity**
81 | >>(string)(Optional)
Conductivity of the plant. Measured in µS/cm. Can have a min and max value set optionally.
82 | >
83 | >**brightness**
84 | >>(string)(Optional)
Light exposure of the plant. Measured in Lux. Can have a min and max value set optionally.
85 |
86 | #### Examples
87 |
88 | ```yaml
89 | - platform: huahuacaocao
90 | plant_id: "zamioculcas zamiifolia"
91 | name: "Plant Zamioculcas Zamiifolia"
92 | sensors:
93 | moisture: sensor.zamioculcas_zamiifolia_moisture
94 | battery: sensor.zamioculcas_zamiifolia_battery
95 | temperature: sensor.zamioculcas_zamiifolia_temperature
96 | conductivity: sensor.zamioculcas_zamiifolia_conductivity
97 | brightness: sensor.zamioculcas_zamiifolia_light_intensity
98 | ```
99 |
100 | ## Integration Examples
101 |
102 | ```yaml
103 | huahuacaocao:
104 | username: !secret huahuacaocao_user
105 | password: !secret huahuacaocao_password
106 | region: EU
107 |
108 | sensor:
109 | - platform: miflora
110 | mac: 'XX:XX:XX:XX:XX:XX'
111 | name: Zamioculcas Zamiifolia
112 | force_update: true
113 | median: 3
114 | monitored_conditions:
115 | - moisture
116 | - light
117 | - temperature
118 | - conductivity
119 | - battery
120 |
121 | - platform: huahuacaocao
122 | plant_id: "zamioculcas zamiifolia"
123 | name: "Plant Zamioculcas Zamiifolia"
124 | sensors:
125 | moisture: sensor.zamioculcas_zamiifolia_moisture
126 | battery: sensor.zamioculcas_zamiifolia_battery
127 | temperature: sensor.zamioculcas_zamiifolia_temperature
128 | conductivity: sensor.zamioculcas_zamiifolia_conductivity
129 | brightness: sensor.zamioculcas_zamiifolia_light_intensity
130 | ```
131 |
132 | Home Assistant Flora Panel
133 |
134 |
135 |
136 | ## Lovelace Configuration
137 |
138 | Import the card using:
139 |
140 | ```yaml
141 | resources:
142 | - url: /hacsfiles/hacs-card-for-xiaomi-mi-flora-and-flower-care/hacs-card-for-xiaomi-mi-flora-and-flower-care.js
143 | type: js
144 | ```
145 | ### Card variables
146 | | **Name** | **Type** | **Requirement** | **Default** | **Description** |
147 | |--------------|---------------|-----------------|-----------------------------------|-----------------------------------------------------------------------------------------------|
148 | | type | string | **Required** | | Card type must be `custom:xiaomi-mi-flora-and-flower-care-card` |
149 | | name | string | Optional | | Card name shown on top |
150 | | zone_name | string | Optional | | Zone name, where the flower is located |
151 | | display | string list | Optional | | Ordered list of sessions. Valid values: `info, maintenance` |
152 | | entity | string | Optional | | huahuacaocao plant sensor name |
153 |
154 | ### Examples
155 |
156 | #### - Lovelace card config with `maintenance` session (without sensors)
157 | ```yaml
158 | type: custom:xiaomi-mi-flora-and-flower-care-card
159 | name: "Zamioculcas Zamiifolia"
160 | entity: plant.plant_zamioculcas_zamiifolia
161 | display:
162 | - maintenance
163 | ```
164 |
165 |
166 |
167 | #### - Lovelace card config with `info` and `maintenance` session (without sensors)
168 |
169 | ```yaml
170 | type: custom:xiaomi-mi-flora-and-flower-care-card
171 | name: "Zamioculcas Zamiifolia"
172 | zone_name: "Kitchen"
173 | display:
174 | - info
175 | - maintenance
176 | ```
177 |
178 |
179 |
180 | #### - Lovelace card config with `info` and `maintenance` session (with sensors)
181 |
182 | ```yaml
183 | type: custom:xiaomi-mi-flora-and-flower-care-card
184 | name: "Zamioculcas Zamiifolia"
185 | entity: plant.plant_zamioculcas_zamiifolia
186 | display:
187 | - maintenance
188 | ```
189 |
190 |
191 |
192 | ##### or with mod-card
193 |
194 | ```yaml
195 | type: custom:mod-card
196 | card:
197 | type: custom:xiaomi-mi-flora-and-flower-care-card
198 | name: "Zamioculcas Zamiifolia"
199 | entity: plant.plant_zamioculcas_zamiifolia
200 | style:|
201 | ha-card {
202 | --primary-text-color: #FFFFFF;
203 | --secondary-text-color: #FFFFFF;
204 | --paper-item-icon-color: #FFFFFF;
205 | --table-tr-td-border-bottom: #FFFFFF;
206 | --banner-background: rgba(50,50,50,0.75);
207 | --content-background: rgba(50,50,50,0.75);
208 | }
209 | ```
210 |
211 |
212 |
213 |
214 |
215 | [license-shield]:https://img.shields.io/github/license/r-renato/hass-xiaomi-mi-flora-and-flower-care
216 | [buymecoffee]: https://www.buymeacoffee.com/0D3WbkKrn
217 | [buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow?style=for-the-badge
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Xiaomi mi flora and Flower care integration
2 |
3 | The Home Assistant custom component uses Flower Care™ Smart Monitor to retrieve flower information (http://www.huahuacaocao.com/product).
4 | HuaHuaCaoCao means flowers & Plants in Chinese.
5 |
6 | [](https://github.com/custom-components/hacs)
7 |
8 | [![License][license-shield]](LICENSE)
9 | [](https://lgtm.com/projects/g/r-renato/hass-xiaomi-mi-flora-and-flower-care/alerts/)
10 | [](https://lgtm.com/projects/g/r-renato/hass-xiaomi-mi-flora-and-flower-care/context:python)
11 |
12 | [![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
13 |
14 | ## Please read carefully
15 |
16 | 1. Need to register to Flower Care™ Smart Monitor App
17 | on Google Android devices or
18 | on Apple iOS devices to use this component.
19 | 2. _This custom component extends the Home Assistant Plant monitor with plant information coming from the Flora Care application._
20 | 3. _The lovelace card is self installed by the component and can be used also without the xiaomi Flora sensor._
21 |
22 |
23 |
24 |
25 |
26 |
27 | ## Manual installation
28 |
29 | 1. Using the tool of choice, open the directory (folder) of your HA configuration (there you can find `configuration.yaml`).
30 | 2. If you do not have a `custom_components` directory (folder) there, you need to create it.
31 | 3. In the `custom_components` directory (folder) create a new folder called `huahuacaocao`.
32 | 4. Download _all_ the files from the `custom_components/huahuacaocao/` directory (folder) in this repository.
33 | 5. Place the files you downloaded in the new directory (folder) you created.
34 |
35 | ## Configuration
36 |
37 | 1. Install your preferred Flower Care™ Smart Monitor App
38 | 2. Register your credentials in App, the same credentials will be used to configure the `huahuacaocao` integration component
39 |
40 | ### Component variables
41 |
42 | **username**
43 | >(string)(Required)
The username to use with your Flower Care™ Smart Monitor App.
44 |
45 | **password**
46 | >(string)(Required)
The corresponding password in yourFlower Care™ Smart Monitor App.
47 |
48 | **region**
49 | >(string)(Optional)
Your country code (two-letter)
50 |
51 | #### Examples
52 |
53 | ```yaml
54 | huahuacaocao:
55 | username: !secret huahuacaocao_user
56 | password: !secret huahuacaocao_password
57 | region: EU
58 | ```
59 |
60 | ### Sensor variables
61 |
62 | **plant_id**
63 | >(string)(Required)
Plant alias. You can find it in the Plant Archive panel of the Flower Care™ Smart Monitor App
64 | >
65 | **Name**
66 | >(string)(Required)
Name to use in the frontend.
67 | >
68 | **sensors**
69 | >(list)(Required)
List of sensor measure entities.
70 | >
71 | >**moisture**
72 | >>(string)(Optional)
Moisture of the plant. Measured in %. Can have a min and max value set optionally.
73 | >
74 | >**battery**
75 | >>(string)(Optional)
Battery level of the plant sensor. Measured in %. Can only have a min level set optionally.
76 | >
77 | >**temperature**
78 | >>(string)(Optional)
Temperature of the plant. Measured in degrees Celsius. Can have a min and max value set optionally.
79 | >
80 | >**conductivity**
81 | >>(string)(Optional)
Conductivity of the plant. Measured in µS/cm. Can have a min and max value set optionally.
82 | >
83 | >**brightness**
84 | >>(string)(Optional)
Light exposure of the plant. Measured in Lux. Can have a min and max value set optionally.
85 |
86 | #### Examples
87 |
88 | ```yaml
89 | - platform: huahuacaocao
90 | plant_id: "zamioculcas zamiifolia"
91 | name: "Plant Zamioculcas Zamiifolia"
92 | sensors:
93 | moisture: sensor.zamioculcas_zamiifolia_moisture
94 | battery: sensor.zamioculcas_zamiifolia_battery
95 | temperature: sensor.zamioculcas_zamiifolia_temperature
96 | conductivity: sensor.zamioculcas_zamiifolia_conductivity
97 | brightness: sensor.zamioculcas_zamiifolia_light_intensity
98 | ```
99 |
100 | ## Integration Examples
101 |
102 | ```yaml
103 | huahuacaocao:
104 | username: !secret huahuacaocao_user
105 | password: !secret huahuacaocao_password
106 | region: EU
107 |
108 | sensor:
109 | - platform: miflora
110 | mac: 'XX:XX:XX:XX:XX:XX'
111 | name: Zamioculcas Zamiifolia
112 | force_update: true
113 | median: 3
114 | monitored_conditions:
115 | - moisture
116 | - light
117 | - temperature
118 | - conductivity
119 | - battery
120 |
121 | - platform: huahuacaocao
122 | plant_id: "zamioculcas zamiifolia"
123 | name: "Plant Zamioculcas Zamiifolia"
124 | sensors:
125 | moisture: sensor.zamioculcas_zamiifolia_moisture
126 | battery: sensor.zamioculcas_zamiifolia_battery
127 | temperature: sensor.zamioculcas_zamiifolia_temperature
128 | conductivity: sensor.zamioculcas_zamiifolia_conductivity
129 | brightness: sensor.zamioculcas_zamiifolia_light_intensity
130 | ```
131 |
132 | Home Assistant Flora Panel
133 |
134 |
135 |
136 | ## Lovelace Configuration
137 | To add , perform the following:
138 |
139 | 1. In the Home Assistant UI, go to `Configuration >> Lovelace Dashboards >> Resources` tab
140 | 2. Click the `+` button
141 | 3. Under `url`, copy and paste the following URL:
142 |
143 | ```
144 | /hacsfiles/hacs-card-for-xiaomi-mi-flora-and-flower-care/hacs-card-for-xiaomi-mi-flora-and-flower-care.js
145 | ```
146 |
147 | 4. Under Resource Type, select 'Javascript Module'.
148 |
149 | If you are using YAML to manage your Lovelace UI, import the card using:
150 |
151 | ```yaml
152 | resources:
153 | - url: /hacsfiles/hacs-card-for-xiaomi-mi-flora-and-flower-care/hacs-card-for-xiaomi-mi-flora-and-flower-care.js
154 | type: js
155 | ```
156 | ### Card variables
157 | | **Name** | **Type** | **Requirement** | **Default** | **Description** |
158 | |--------------|---------------|-----------------|-----------------------------------|-----------------------------------------------------------------------------------------------|
159 | | type | string | **Required** | | Card type must be `custom:xiaomi-mi-flora-and-flower-care-card` |
160 | | name | string | Optional | | Card name shown on top |
161 | | zone_name | string | Optional | | Zone name, where the flower is located |
162 | | display | string list | Optional | | Ordered list of sessions. Valid values: `info, maintenance` |
163 | | entity | string | Optional | | huahuacaocao plant sensor name |
164 |
165 | ### Examples
166 |
167 | #### - Lovelace card config with `maintenance` session (without sensors)
168 | ```yaml
169 | type: custom:xiaomi-mi-flora-and-flower-care-card
170 | name: "Zamioculcas Zamiifolia"
171 | entity: plant.plant_zamioculcas_zamiifolia
172 | display:
173 | - maintenance
174 | ```
175 |
176 |
177 |
178 | #### - Lovelace card config with `info` and `maintenance` session (without sensors)
179 |
180 | ```yaml
181 | type: custom:xiaomi-mi-flora-and-flower-care-card
182 | name: "Zamioculcas Zamiifolia"
183 | zone_name: "Kitchen"
184 | display:
185 | - info
186 | - maintenance
187 | ```
188 |
189 |
190 |
191 | #### - Lovelace card config with `info` and `maintenance` session (with sensors)
192 |
193 | ```yaml
194 | type: custom:xiaomi-mi-flora-and-flower-care-card
195 | name: "Zamioculcas Zamiifolia"
196 | entity: plant.plant_zamioculcas_zamiifolia
197 | display:
198 | - maintenance
199 | ```
200 |
201 |
202 |
203 | ##### or with mod-card
204 |
205 | ```yaml
206 | type: custom:mod-card
207 | card:
208 | type: custom:xiaomi-mi-flora-and-flower-care-card
209 | name: "Zamioculcas Zamiifolia"
210 | entity: plant.plant_zamioculcas_zamiifolia
211 | style:|
212 | ha-card {
213 | --primary-text-color: #FFFFFF;
214 | --secondary-text-color: #FFFFFF;
215 | --paper-item-icon-color: #FFFFFF;
216 | --table-tr-td-border-bottom: #FFFFFF;
217 | --banner-background: rgba(50,50,50,0.75);
218 | --content-background: rgba(50,50,50,0.75);
219 | }
220 | ```
221 |
222 |
223 |
224 |
225 |
226 | [license-shield]:https://img.shields.io/github/license/r-renato/hass-xiaomi-mi-flora-and-flower-care
227 | [buymecoffee]: https://www.buymeacoffee.com/0D3WbkKrn
228 | [buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow?style=for-the-badge
229 |
--------------------------------------------------------------------------------
/src/hacs-card-for-xiaomi-mi-flora-and-flower-care.ts:
--------------------------------------------------------------------------------
1 | import {
2 | LitElement,
3 | html,
4 | customElement,
5 | property,
6 | CSSResult,
7 | TemplateResult,
8 | css,
9 | PropertyValues
10 | } from "lit-element";
11 |
12 | import {
13 | HomeAssistant,
14 | handleClick,
15 | longPress,
16 | hasConfigOrEntityChanged
17 | } from "custom-card-helpers";
18 |
19 | import {CardConfig, FloraCarePlant, FloraCarePlantRanges, Sensor} from "./types";
20 |
21 | import style from './style';
22 |
23 | console.info("%c XIAOMI-MI-FLORA-AND-FLOWER-CARE-CARD %c 1.2.0 ", "color: white; background: green; font-weight: 700;", "color: coral; background: white; font-weight: 700;");
24 |
25 | @customElement("xiaomi-mi-flora-and-flower-care-card")
26 | class FlowerCareCard extends LitElement {
27 | private invalidConfig: boolean = false ;
28 | private invalidEntity: boolean = false ;
29 |
30 | private displayInfo: boolean = false ;
31 | private displayMaintenance: boolean = false ;
32 |
33 | readonly MOINSTURE = 'moisture' ;
34 | readonly CONDUCTIVITY = 'conductivity' ;
35 | readonly BRIGHTNESS = 'brightness' ;
36 | readonly TEMPERATURE = 'temperature' ;
37 | readonly BATTERY = 'battery' ;
38 |
39 | @property() public hass?: HomeAssistant ;
40 |
41 | @property() private _config?: CardConfig ;
42 |
43 | @property() private _floraCare?: FloraCarePlant ;
44 | @property() private _floraRanges?: FloraCarePlantRanges ;
45 |
46 | /**
47 | *
48 | * @param {CardConfig} config
49 | */
50 | public setConfig(config: CardConfig): void {
51 | console.log({ flora_care_card_config: config });
52 |
53 | if (!config) {
54 | this.invalidConfig = true ;
55 | throw new Error("Invalid configuration") ;
56 | }
57 |
58 | if (!config.entity || config.entity.length == 0) {
59 | this.invalidEntity = true ;
60 | throw new Error('Entity is required') ;
61 | }
62 |
63 | if (config.display && config.display.length > 0) {
64 | let displays = config.display.map(function(value) {
65 | return value.toLocaleLowerCase() ;
66 | }) ;
67 |
68 | this.displayMaintenance = displays.includes('maintenance') ;
69 | this.displayInfo = displays.includes('info') ;
70 | }
71 | this._config = config;
72 | }
73 |
74 | /**
75 | * get the current size of the card
76 | * @return {Number}
77 | */
78 | getCardSize() {
79 | return 1;
80 | }
81 |
82 | /**
83 | *
84 | * @returns {CSSResult}
85 | */
86 | static get styles(): CSSResult {
87 | return style;
88 | }
89 |
90 | /**
91 | * generates the card HTML
92 | * @return {TemplateResult}
93 | */
94 | render() {
95 | if ( this.invalidConfig || this.invalidEntity ) return html`
96 |
97 |
98 |
99 |
100 |
101 | Configuration ERROR!
102 |
103 |
104 | `;
105 | else return this._render() ;
106 | }
107 |
108 | /**
109 | *
110 | * @returns {TemplateResult}
111 | * @private
112 | */
113 | _render() {
114 | this._floraCare = this.hass.states[ this._config!.entity ] ;
115 | this._floraRanges = this._floraCare.attributes.ranges ;
116 |
117 | let content = this.computeContent() ;
118 | let displayContent = "" !== content.strings.raw.toString() ;
119 | let infoClass = this.displayInfo && (this.displayMaintenance || displayContent) ? 'banner div-border-bottom' : 'banner';
120 | let maintenanceClass = this.displayMaintenance && displayContent ? 'banner div-border-bottom' : 'banner';
121 |
122 | return html`
123 |
124 | ${this.computeHeader()}
125 |
126 | ${this.displayInfo ? html`
127 | ${this.computeInfoToolTips(this._floraCare.attributes.info)}
128 | ` : html`` }
129 | ${this.displayMaintenance ? html`
130 | ${this.computeMaintenanceToolTips(this._floraCare.attributes.maintenance)}
131 | ` : html`` }
132 | ${displayContent ? html`
133 |
134 | ${content}
135 |
136 | ` : html`` }
137 |
138 | `;
139 | }
140 |
141 | /**
142 | *
143 | * @returns {TemplateResult}
144 | */
145 | computeContent() {
146 | //console.log({ configconfig: this.hass.states[ this._config!.entity ]});
147 | //console.log({ config2: this.hass.states[ 'sensor.zamioculcas_zamiifolia_battery' ]});
148 |
149 | //console.log({ floracaresensor: floraCare.attributes.sensors });
150 |
151 | let moinsture = this.getSensor( this._floraCare.attributes.sensors[ this.MOINSTURE ]) ;
152 | let conductivity = this.getSensor( this._floraCare.attributes.sensors[ this.CONDUCTIVITY ]) ;
153 | let brightness = this.getSensor( this._floraCare.attributes.sensors[ this.BRIGHTNESS ]) ;
154 | let temperature = this.getSensor( this._floraCare.attributes.sensors[ this.TEMPERATURE ]) ;
155 |
156 | if( !moinsture && !conductivity && !brightness && !temperature )
157 | return html`` ;
158 | else
159 | return html`
160 | ${moinsture ? this.computeContentItem(
161 | moinsture,
162 | this._floraRanges.min_soil_moist, this._floraRanges.max_soil_moist,
163 | this.computeAttributeClass( this._floraCare.attributes.problem, moinsture, this.MOINSTURE ),
164 | 'mdi:water-percent'
165 | ) : html``}
166 | ${conductivity ? this.computeContentItem(
167 | conductivity,
168 | this._floraRanges.min_soil_ec, this._floraRanges.max_soil_ec,
169 | this.computeAttributeClass( this._floraCare.attributes.problem, conductivity, this.CONDUCTIVITY ),
170 | 'mdi:flash-circle'
171 | ): html``}
172 | ${brightness ? this.computeContentItem(
173 | brightness,
174 | this._floraRanges.min_light_lux, this._floraRanges.max_light_lux,
175 | this.computeAttributeClass( this._floraCare.attributes.problem, brightness, this.BRIGHTNESS ),
176 | 'mdi:white-balance-sunny'
177 | ): html``}
178 | ${temperature ? this.computeContentItem(
179 | temperature,
180 | this._floraRanges.min_temp, this._floraRanges.max_temp,
181 | this.computeAttributeClass( this._floraCare.attributes.problem, temperature, this.TEMPERATURE ),
182 | 'mdi:thermometer'
183 | ): html``}
184 | `;
185 | }
186 |
187 | computeTitle() {
188 | return (this._config && this._config.name) ;
189 | }
190 |
191 | /**
192 | *
193 | * @param {Sensor} sensor
194 | * @param {number} min
195 | * @param {number} max
196 | * @param {string} problemClass
197 | * @returns {TemplateResult}
198 | */
199 | computeContentItem( sensor, min : number, max : number, problemClass: string, defaultIcon: string ) {
200 | if( undefined !== sensor ) {
201 | let icon = sensor.attributes && sensor.attributes.icon
202 | ? sensor.attributes.icon : defaultIcon ;
203 |
204 | return html`
205 | this.handlePopup(e, sensor.entity_id)}>
206 |
207 |
208 |
209 |
210 | ${sensor.state.indexOf("unknown") === -1
211 | ? sensor.state + " " + sensor.attributes.unit_of_measurement : "n/a"}
212 |
213 |
214 | ${null !== min && null !== max ? html`${min}-${max}` : html``}
215 |
216 |
217 | `;
218 | } else {
219 | return html`
220 | `;
221 | }
222 | }
223 |
224 | computeMaintenanceToolTips( maintenance ) {
225 | return html`
226 |
227 |
228 | | Maintenance |
229 |
230 |
231 | ${Object.keys(maintenance).map( key => html`
232 |
233 | | ${this.capitalize(key)} |
234 | ${maintenance[key]} |
235 |
236 | `)}
237 |
238 |
239 | `;
240 | }
241 |
242 | computeInfoToolTips( info ) {
243 | return html`
244 |
245 |
246 | ${Object.keys(info).filter( key => {
247 | if( 'floral_language' == key ) {
248 | return false; // skip
249 | }
250 | return true;
251 | }).map( key => html`
252 |
253 | | ${this.capitalize(key)} |
254 | ${info[key]} |
255 |
256 | `)}
257 |
258 |
259 | `;
260 | }
261 |
262 | computeHeader() {
263 | let batterySensor = this.getSensor( this._floraCare.attributes.sensors[ this.BATTERY ]) ;
264 | return html`
265 |
266 |
267 |
268 |
269 | |
270 |
278 | |
279 |
280 |
288 | |
289 |
290 | ${this.computeContentItem(batterySensor, null, null,
291 | this.computeAttributeClass( this._floraCare.attributes.problem, batterySensor, this.BATTERY ),
292 | 'mdi:battery-charging'
293 | )}
294 | |
295 |
296 |
297 |
298 |
299 |
300 |
301 | `;
302 | }
303 |
304 | /**
305 | *
306 | * @param problem
307 | * @param attr
308 | * @returns {string}
309 | */
310 | computeAttributeClass(problem, state, attr) {
311 | return problem.indexOf(attr) === -1
312 | && undefined !== state && state.state.indexOf("unknown") === -1 ? "" : "problem";
313 | }
314 |
315 | /* private _handleTap(): void {
316 | handleClick(this, this.hass!, this._config!, false);
317 | }
318 |
319 | private _handleHold(): void {
320 | handleClick(this, this.hass!, this._config!, true);
321 | }*/
322 |
323 | /**
324 | *
325 | * @param {string} name
326 | */
327 | getSensor( name : string ) : Sensor {
328 | let sensor:Sensor = this.hass.states[ name ] ;
329 |
330 | // console.log({ sensor: sensor });
331 | return( sensor ) ;
332 | }
333 |
334 | capitalize(string) {
335 | return string.charAt(0).toUpperCase() + string.slice(1);
336 | }
337 |
338 | /**
339 | *
340 | * @param e
341 | * @param entityId
342 | */
343 | handlePopup(e, entityId: string) {
344 | e.stopPropagation();
345 |
346 | let ne = new Event('hass-more-info', {composed: true});
347 | // @ts-ignore
348 | ne.detail = {entityId};
349 | this.dispatchEvent(ne);
350 | }
351 | }
352 |
353 |
354 |
--------------------------------------------------------------------------------
/custom_components/huahuacaocao/sensor.py:
--------------------------------------------------------------------------------
1 | """Support for monitoring plants."""
2 | from collections import deque
3 | from datetime import datetime, timedelta
4 | import logging
5 |
6 | import voluptuous as vol
7 |
8 | from homeassistant.components import group
9 | from homeassistant.components.recorder.util import execute, session_scope
10 | from homeassistant.const import (
11 | ATTR_TEMPERATURE,
12 | ATTR_UNIT_OF_MEASUREMENT,
13 | CONF_NAME,
14 | CONF_SENSORS,
15 | STATE_OK,
16 | STATE_PROBLEM,
17 | STATE_UNAVAILABLE,
18 | STATE_UNKNOWN,
19 | TEMP_CELSIUS,
20 | )
21 | from homeassistant.core import callback
22 | from homeassistant.exceptions import HomeAssistantError
23 | import homeassistant.helpers.config_validation as cv
24 | from homeassistant.helpers.entity import Entity
25 | from homeassistant.helpers.entity_component import EntityComponent
26 | from homeassistant.helpers.event import async_track_state_change
27 |
28 | """
29 | My import
30 | """
31 | from . import DOMAIN
32 |
33 | _LOGGER = logging.getLogger(__name__)
34 |
35 | DEFAULT_NAME = "plant"
36 |
37 | READING_BATTERY = "battery"
38 | READING_TEMPERATURE = ATTR_TEMPERATURE
39 | READING_MOISTURE = "moisture"
40 | READING_CONDUCTIVITY = "conductivity"
41 | READING_BRIGHTNESS = "brightness"
42 |
43 | ATTR_PROBLEM = "problem"
44 | ATTR_SENSORS = "sensors"
45 | PROBLEM_NONE = "none"
46 | ATTR_MAX_BRIGHTNESS_HISTORY = "max_brightness"
47 |
48 | # we're not returning only one value, we're returning a dict here. So we need
49 | # to have a separate literal for it to avoid confusion.
50 | ATTR_DICT_OF_UNITS_OF_MEASUREMENT = "unit_of_measurement_dict"
51 |
52 | CONF_MIN_BATTERY_LEVEL = "min_" + READING_BATTERY
53 | CONF_MIN_TEMPERATURE = "min_" + READING_TEMPERATURE
54 | CONF_MAX_TEMPERATURE = "max_" + READING_TEMPERATURE
55 | CONF_MIN_MOISTURE = "min_" + READING_MOISTURE
56 | CONF_MAX_MOISTURE = "max_" + READING_MOISTURE
57 | CONF_MIN_CONDUCTIVITY = "min_" + READING_CONDUCTIVITY
58 | CONF_MAX_CONDUCTIVITY = "max_" + READING_CONDUCTIVITY
59 | CONF_MIN_BRIGHTNESS = "min_" + READING_BRIGHTNESS
60 | CONF_MAX_BRIGHTNESS = "max_" + READING_BRIGHTNESS
61 | CONF_CHECK_DAYS = "check_days"
62 |
63 | CONF_SENSOR_BATTERY_LEVEL = READING_BATTERY
64 | CONF_SENSOR_MOISTURE = READING_MOISTURE
65 | CONF_SENSOR_CONDUCTIVITY = READING_CONDUCTIVITY
66 | CONF_SENSOR_TEMPERATURE = READING_TEMPERATURE
67 | CONF_SENSOR_BRIGHTNESS = READING_BRIGHTNESS
68 |
69 | DEFAULT_MIN_BATTERY_LEVEL = 20
70 | DEFAULT_MIN_MOISTURE = 20
71 | DEFAULT_MAX_MOISTURE = 60
72 | DEFAULT_MIN_CONDUCTIVITY = 500
73 | DEFAULT_MAX_CONDUCTIVITY = 3000
74 | DEFAULT_CHECK_DAYS = 3
75 |
76 | """
77 | My constant
78 | """
79 | ATTR_INFO = "info"
80 | ATTR_BASIC = "basic"
81 | ATTR_MAINTENANCE = "maintenance"
82 | ATTR_IMAGE = "image"
83 |
84 | CONF_PLANT_ID = "plant_id"
85 | SERVICE_API = 'flower_service'
86 |
87 | API_PARAMETER = "parameter"
88 | API_PARAMETER_MIN_TEMP = "min_temp"
89 | API_PARAMETER_MAX_TEMP = "max_temp"
90 | API_PARAMETER_MIN_SOIL_MOIST = "min_soil_moist"
91 | API_PARAMETER_MAX_SOIL_MOIST = "max_soil_moist"
92 | API_PARAMETER_MIN_SOIL_EC = "min_soil_ec"
93 | API_PARAMETER_MAX_SOIL_EC = "max_soil_ec"
94 | API_PARAMETER_MIN_LIGHT_LUX = "min_light_lux"
95 | API_PARAMETER_MAX_LIGHT_LUX = "max_light_lux"
96 |
97 | ATTR_FLOWER_CARE_DATA = "flower_care_data"
98 | ATTR_RANGES = "ranges"
99 |
100 | SCHEMA_SENSORS = vol.Schema(
101 | {
102 | vol.Optional(CONF_SENSOR_BATTERY_LEVEL): cv.entity_id,
103 | vol.Optional(CONF_SENSOR_MOISTURE): cv.entity_id,
104 | vol.Optional(CONF_SENSOR_CONDUCTIVITY): cv.entity_id,
105 | vol.Optional(CONF_SENSOR_TEMPERATURE): cv.entity_id,
106 | vol.Optional(CONF_SENSOR_BRIGHTNESS): cv.entity_id,
107 | }
108 | )
109 |
110 | DOMAIN_PLANT = "plant"
111 | GROUP_NAME_ALL_PLANTS = "all plants"
112 | ENTITY_ID_ALL_PLANTS = group.ENTITY_ID_FORMAT.format("all_plants")
113 |
114 | """
115 | My schemas
116 | """
117 | PLANT_SCHEMA = vol.Schema(
118 | {
119 | vol.Optional(CONF_PLANT_ID): cv.string,
120 | vol.Required(CONF_NAME): cv.string,
121 | vol.Optional(CONF_SENSORS): vol.Schema(SCHEMA_SENSORS),
122 | }
123 | )
124 |
125 | PLATFORM_SCHEMA = vol.Schema({DOMAIN: {cv.string: PLANT_SCHEMA}}, extra=vol.ALLOW_EXTRA)
126 |
127 | # Flag for enabling/disabling the loading of the history from the database.
128 | # This feature is turned off right now as its tests are not 100% stable.
129 | ENABLE_LOAD_HISTORY = False
130 |
131 |
132 | def setup_platform(hass, config, add_devices, discovery_info=None):
133 | """Set up the Plant component."""
134 | _LOGGER.info("__init__ setup_platform 'sensor' start for %s with config %s.", DOMAIN, config)
135 |
136 | if config.get(CONF_PLANT_ID) is None:
137 | return True
138 |
139 | if not hasattr(hass.data[DOMAIN], ATTR_FLOWER_CARE_DATA):
140 | hass.data[DOMAIN][ATTR_FLOWER_CARE_DATA] = {}
141 |
142 | params = {}
143 | sensors = config.get(CONF_SENSORS)
144 |
145 | if not (sensors is None):
146 | params[CONF_SENSORS] = sensors
147 | else:
148 | params[CONF_SENSORS] = dict()
149 |
150 | plant_key = config.get(CONF_PLANT_ID).replace(" ", "_")
151 | app_api = hass.data[DOMAIN][SERVICE_API]
152 |
153 | if not (app_api is None):
154 | if not hasattr(hass.data[DOMAIN][ATTR_FLOWER_CARE_DATA], plant_key):
155 | flower_info = app_api.retrieve_flower_details(config.get(CONF_PLANT_ID))
156 | _LOGGER.debug("__init__ setup_platform 'sensor' start for %s. Retrieved Flower: %s", DOMAIN, flower_info)
157 |
158 | if not (flower_info is None):
159 | hass.data[DOMAIN][ATTR_FLOWER_CARE_DATA][plant_key] = flower_info
160 | else:
161 | flower_info = hass.data[DOMAIN][ATTR_FLOWER_CARE_DATA][plant_key]
162 | _LOGGER.debug("__init__ setup_platform 'sensor' start for %s. Reused Flower: %s", DOMAIN, flower_info)
163 |
164 | if not (flower_info is None):
165 | params[ATTR_RANGES] = flower_info[API_PARAMETER]
166 | params[CONF_MIN_TEMPERATURE] = flower_info[API_PARAMETER][API_PARAMETER_MIN_TEMP]
167 | params[CONF_MAX_TEMPERATURE] = flower_info[API_PARAMETER][API_PARAMETER_MAX_TEMP]
168 | params[CONF_MIN_MOISTURE] = flower_info[API_PARAMETER][API_PARAMETER_MIN_SOIL_MOIST]
169 | params[CONF_MAX_MOISTURE] = flower_info[API_PARAMETER][API_PARAMETER_MAX_SOIL_MOIST]
170 | params[CONF_MIN_CONDUCTIVITY] = flower_info[API_PARAMETER][API_PARAMETER_MIN_SOIL_EC]
171 | params[CONF_MAX_CONDUCTIVITY] = flower_info[API_PARAMETER][API_PARAMETER_MAX_SOIL_EC]
172 | params[CONF_MIN_BRIGHTNESS] = flower_info[API_PARAMETER][API_PARAMETER_MIN_LIGHT_LUX]
173 | params[CONF_MAX_BRIGHTNESS] = flower_info[API_PARAMETER][API_PARAMETER_MAX_LIGHT_LUX]
174 |
175 | params[ATTR_BASIC] = flower_info[ATTR_BASIC]
176 | params[ATTR_MAINTENANCE] = flower_info[ATTR_MAINTENANCE]
177 | params[ATTR_IMAGE] = flower_info[ATTR_IMAGE]
178 |
179 | # if not (sensors is None):
180 | component = EntityComponent(_LOGGER, DOMAIN_PLANT, hass)
181 |
182 | entities = []
183 |
184 | name = config.get(CONF_NAME)
185 | entity = Plant(name, params)
186 | entities.append(entity)
187 |
188 | component.add_entities(entities)
189 | _LOGGER.debug("__init__ setup_platform 'sensor' %s in platform %s. Component added with %s.", name, DOMAIN, params)
190 |
191 | _LOGGER.info("__init__ setup_platform 'sensor' done for %s.", DOMAIN)
192 | return True
193 |
194 |
195 | class Plant(Entity):
196 | """Plant monitors the well-being of a plant.
197 |
198 | It also checks the measurements against
199 | configurable min and max values.
200 | """
201 |
202 | READINGS = {
203 | READING_BATTERY: {ATTR_UNIT_OF_MEASUREMENT: "%", "min": CONF_MIN_BATTERY_LEVEL},
204 | READING_TEMPERATURE: {
205 | ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
206 | "min": CONF_MIN_TEMPERATURE,
207 | "max": CONF_MAX_TEMPERATURE,
208 | },
209 | READING_MOISTURE: {
210 | ATTR_UNIT_OF_MEASUREMENT: "%",
211 | "min": CONF_MIN_MOISTURE,
212 | "max": CONF_MAX_MOISTURE,
213 | },
214 | READING_CONDUCTIVITY: {
215 | ATTR_UNIT_OF_MEASUREMENT: "µS/cm",
216 | "min": CONF_MIN_CONDUCTIVITY,
217 | "max": CONF_MAX_CONDUCTIVITY,
218 | },
219 | READING_BRIGHTNESS: {
220 | ATTR_UNIT_OF_MEASUREMENT: "lux",
221 | "min": CONF_MIN_BRIGHTNESS,
222 | "max": CONF_MAX_BRIGHTNESS,
223 | },
224 | }
225 |
226 | def __init__(self, name, config):
227 | """Initialize the Plant component."""
228 | self._config = config
229 | self._sensormap = dict()
230 | self._readingmap = dict()
231 | self._unit_of_measurement = dict()
232 | for reading, entity_id in config["sensors"].items():
233 | self._sensormap[entity_id] = reading
234 | self._readingmap[reading] = entity_id
235 | self._state = None
236 | self._name = name
237 | self._battery = None
238 | self._moisture = None
239 | self._conductivity = None
240 | self._temperature = None
241 | self._brightness = None
242 | self._problems = PROBLEM_NONE
243 |
244 | self._conf_check_days = 3 # default check interval: 3 days
245 | if CONF_CHECK_DAYS in self._config:
246 | self._conf_check_days = self._config[CONF_CHECK_DAYS]
247 | self._brightness_history = DailyHistory(self._conf_check_days)
248 |
249 | @callback
250 | def state_changed(self, entity_id, _, new_state):
251 | """Update the sensor status.
252 |
253 | This callback is triggered, when the sensor state changes.
254 | """
255 | value = new_state.state
256 | _LOGGER.debug("Received callback from %s with value %s", entity_id, value)
257 | if value == STATE_UNKNOWN:
258 | return
259 |
260 | reading = self._sensormap[entity_id]
261 | if reading == READING_MOISTURE:
262 | if value != STATE_UNAVAILABLE:
263 | value = int(float(value))
264 | self._moisture = value
265 | elif reading == READING_BATTERY:
266 | if value != STATE_UNAVAILABLE:
267 | value = int(float(value))
268 | self._battery = value
269 | elif reading == READING_TEMPERATURE:
270 | if value != STATE_UNAVAILABLE:
271 | value = float(value)
272 | self._temperature = value
273 | elif reading == READING_CONDUCTIVITY:
274 | if value != STATE_UNAVAILABLE:
275 | value = int(float(value))
276 | self._conductivity = value
277 | elif reading == READING_BRIGHTNESS:
278 | if value != STATE_UNAVAILABLE:
279 | value = int(float(value))
280 | self._brightness = value
281 | self._brightness_history.add_measurement(
282 | self._brightness, new_state.last_updated
283 | )
284 | else:
285 | raise HomeAssistantError(
286 | f"Unknown reading from sensor {entity_id}: {value}"
287 | )
288 | if ATTR_UNIT_OF_MEASUREMENT in new_state.attributes:
289 | self._unit_of_measurement[reading] = new_state.attributes.get(
290 | ATTR_UNIT_OF_MEASUREMENT
291 | )
292 | self._update_state()
293 |
294 | def _update_state(self):
295 | """Update the state of the class based sensor data."""
296 | result = []
297 | for sensor_name in self._sensormap.values():
298 | params = self.READINGS[sensor_name]
299 | value = getattr(self, f"_{sensor_name}")
300 | if value is not None:
301 | if value == STATE_UNAVAILABLE:
302 | result.append(f"{sensor_name} unavailable")
303 | else:
304 | if sensor_name == READING_BRIGHTNESS:
305 | result.append(
306 | self._check_min(
307 | sensor_name, self._brightness_history.max, params
308 | )
309 | )
310 | else:
311 | result.append(self._check_min(sensor_name, value, params))
312 | result.append(self._check_max(sensor_name, value, params))
313 |
314 | result = [r for r in result if r is not None]
315 |
316 | if result:
317 | self._state = STATE_PROBLEM
318 | self._problems = ", ".join(result)
319 | else:
320 | self._state = STATE_OK
321 | self._problems = PROBLEM_NONE
322 | _LOGGER.debug("New data processed")
323 | self.async_schedule_update_ha_state()
324 |
325 | def _check_min(self, sensor_name, value, params):
326 | """If configured, check the value against the defined minimum value."""
327 | if "min" in params and params["min"] in self._config:
328 | min_value = self._config[params["min"]]
329 | if value < min_value:
330 | return f"{sensor_name} low"
331 |
332 | def _check_max(self, sensor_name, value, params):
333 | """If configured, check the value against the defined maximum value."""
334 | if "max" in params and params["max"] in self._config:
335 | max_value = self._config[params["max"]]
336 | if value > max_value:
337 | return f"{sensor_name} high"
338 | return None
339 |
340 | async def async_added_to_hass(self):
341 | """After being added to hass, load from history."""
342 | if ENABLE_LOAD_HISTORY and "recorder" in self.hass.config.components:
343 | # only use the database if it's configured
344 | self.hass.async_add_job(self._load_history_from_db)
345 |
346 | async_track_state_change(self.hass, list(self._sensormap), self.state_changed)
347 |
348 | for entity_id in self._sensormap:
349 | state = self.hass.states.get(entity_id)
350 | if state is not None:
351 | self.state_changed(entity_id, None, state)
352 |
353 | async def _load_history_from_db(self):
354 | """Load the history of the brightness values from the database.
355 |
356 | This only needs to be done once during startup.
357 | """
358 | from homeassistant.components.recorder.models import States
359 |
360 | start_date = datetime.now() - timedelta(days=self._conf_check_days)
361 | entity_id = self._readingmap.get(READING_BRIGHTNESS)
362 | if entity_id is None:
363 | _LOGGER.debug(
364 | "Not reading the history from the database as "
365 | "there is no brightness sensor configured"
366 | )
367 | return
368 |
369 | _LOGGER.debug("Initializing values for %s from the database", self._name)
370 | with session_scope(hass=self.hass) as session:
371 | query = (
372 | session.query(States)
373 | .filter(
374 | (States.entity_id == entity_id.lower())
375 | and (States.last_updated > start_date)
376 | )
377 | .order_by(States.last_updated.asc())
378 | )
379 | states = execute(query)
380 |
381 | for state in states:
382 | # filter out all None, NaN and "unknown" states
383 | # only keep real values
384 | try:
385 | self._brightness_history.add_measurement(
386 | int(state.state), state.last_updated
387 | )
388 | except ValueError:
389 | pass
390 | _LOGGER.debug("Initializing from database completed")
391 | self.async_schedule_update_ha_state()
392 |
393 | @property
394 | def should_poll(self):
395 | """No polling needed."""
396 | return False
397 |
398 | @property
399 | def name(self):
400 | """Return the name of the sensor."""
401 | return self._name
402 |
403 | @property
404 | def state(self):
405 | """Return the state of the entity."""
406 | return self._state
407 |
408 | @property
409 | def state_attributes(self):
410 | """Return the attributes of the entity.
411 |
412 | Provide the individual measurements from the
413 | sensor in the attributes of the device.
414 | """
415 | attrib = {
416 | ATTR_PROBLEM: self._problems,
417 | ATTR_SENSORS: self._readingmap,
418 | ATTR_DICT_OF_UNITS_OF_MEASUREMENT: self._unit_of_measurement,
419 | ATTR_INFO: self._config[ATTR_BASIC],
420 | ATTR_MAINTENANCE: self._config[ATTR_MAINTENANCE],
421 | ATTR_RANGES: self._config[ATTR_RANGES],
422 | ATTR_IMAGE: self._config[ATTR_IMAGE],
423 | }
424 |
425 | for reading in self._sensormap.values():
426 | attrib[reading] = getattr(self, f"_{reading}")
427 |
428 | if self._brightness_history.max is not None:
429 | attrib[ATTR_MAX_BRIGHTNESS_HISTORY] = self._brightness_history.max
430 |
431 | return attrib
432 |
433 |
434 | class DailyHistory:
435 | """Stores one measurement per day for a maximum number of days.
436 |
437 | At the moment only the maximum value per day is kept.
438 | """
439 |
440 | def __init__(self, max_length):
441 | """Create new DailyHistory with a maximum length of the history."""
442 | self.max_length = max_length
443 | self._days = None
444 | self._max_dict = dict()
445 | self.max = None
446 |
447 | def add_measurement(self, value, timestamp=None):
448 | """Add a new measurement for a certain day."""
449 | day = (timestamp or datetime.now()).date()
450 | if not isinstance(value, (int, float)):
451 | return
452 | if self._days is None:
453 | self._days = deque()
454 | self._add_day(day, value)
455 | else:
456 | current_day = self._days[-1]
457 | if day == current_day:
458 | self._max_dict[day] = max(value, self._max_dict[day])
459 | elif day > current_day:
460 | self._add_day(day, value)
461 | else:
462 | _LOGGER.warning("Received old measurement, not storing it")
463 |
464 | self.max = max(self._max_dict.values())
465 |
466 | def _add_day(self, day, value):
467 | """Add a new day to the history.
468 |
469 | Deletes the oldest day, if the queue becomes too long.
470 | """
471 | if len(self._days) == self.max_length:
472 | oldest = self._days.popleft()
473 | del self._max_dict[oldest]
474 | self._days.append(day)
475 | if not isinstance(value, (int, float)):
476 | return
477 | self._max_dict[day] = value
478 |
--------------------------------------------------------------------------------
/custom_components/huahuacaocao/.plugin/hacs-card-for-xiaomi-mi-flora-and-flower-care.js:
--------------------------------------------------------------------------------
1 | function t(t,e,i,s){var n,r=arguments.length,o=r<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(t,e,i,s);else for(var a=t.length-1;a>=0;a--)(n=t[a])&&(o=(r<3?n(o):r>3?n(e,i,o):n(e,i))||o);return r>3&&o&&Object.defineProperty(e,i,o),o}const e=new WeakMap,i=t=>"function"==typeof t&&e.has(t),s=void 0!==window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,n=(t,e,i=null)=>{for(;e!==i;){const i=e.nextSibling;t.removeChild(e),e=i}},r={},o={},a=`{{lit-${String(Math.random()).slice(2)}}}`,l=`\x3c!--${a}--\x3e`,d=new RegExp(`${a}|${l}`),c="$lit$";class h{constructor(t,e){this.parts=[],this.element=e;const i=[],s=[],n=document.createTreeWalker(e.content,133,null,!1);let r=0,o=-1,l=0;const{strings:h,values:{length:u}}=t;for(;l0;){const e=h[l],i=f.exec(e)[2],s=i.toLowerCase()+c,n=t.getAttribute(s);t.removeAttribute(s);const r=n.split(d);this.parts.push({type:"attribute",index:o,name:i,strings:r}),l+=r.length-1}}"TEMPLATE"===t.tagName&&(s.push(t),n.currentNode=t.content)}else if(3===t.nodeType){const e=t.data;if(e.indexOf(a)>=0){const s=t.parentNode,n=e.split(d),r=n.length-1;for(let e=0;e{const i=t.length-e.length;return i>=0&&t.slice(i)===e},u=t=>-1!==t.index,m=()=>document.createComment(""),f=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=\/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;class g{constructor(t,e,i){this.__parts=[],this.template=t,this.processor=e,this.options=i}update(t){let e=0;for(const i of this.__parts)void 0!==i&&i.setValue(t[e]),e++;for(const t of this.__parts)void 0!==t&&t.commit()}_clone(){const t=s?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),e=[],i=this.template.parts,n=document.createTreeWalker(t,133,null,!1);let r,o=0,a=0,l=n.nextNode();for(;o-1||i)&&-1===t.indexOf("--\x3e",n+1);const r=f.exec(t);e+=null===r?t+(i?_:l):t.substr(0,r.index)+r[1]+r[2]+c+r[3]+a}return e+=this.strings[t]}getTemplateElement(){const t=document.createElement("template");return t.innerHTML=this.getHTML(),t}}const y=t=>null===t||!("object"==typeof t||"function"==typeof t),v=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class S{constructor(t,e,i){this.dirty=!0,this.element=t,this.name=e,this.strings=i,this.parts=[];for(let t=0;tthis.handleEvent(t))}setValue(t){this.__pendingValue=t}commit(){for(;i(this.__pendingValue);){const t=this.__pendingValue;this.__pendingValue=r,t(this)}if(this.__pendingValue===r)return;const t=this.__pendingValue,e=this.value,s=null==t||null!=e&&(t.capture!==e.capture||t.once!==e.once||t.passive!==e.passive),n=null!=t&&(null==e||s);s&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),n&&(this.__options=A(t),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=t,this.__pendingValue=r}handleEvent(t){"function"==typeof this.value?this.value.call(this.eventContext||this.element,t):this.value.handleEvent(t)}}const A=t=>t&&(N?{capture:t.capture,passive:t.passive,once:t.once}:t.capture);const R=new class{handleAttributeExpressions(t,e,i,s){const n=e[0];return"."===n?new P(t,e.slice(1),i).parts:"@"===n?[new E(t,e.slice(1),s.eventContext)]:"?"===n?[new C(t,e.slice(1),i)]:new S(t,e,i).parts}handleTextExpression(t){return new x(t)}};function k(t){let e=$.get(t.type);void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},$.set(t.type,e));let i=e.stringsArray.get(t.strings);if(void 0!==i)return i;const s=t.strings.join(a);return void 0===(i=e.keyString.get(s))&&(i=new h(t,t.getTemplateElement()),e.keyString.set(s,i)),e.stringsArray.set(t.strings,i),i}const $=new Map,O=new WeakMap;(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.1.2");const V=(t,...e)=>new b(t,e,"html",R),I=133;function M(t,e){const{element:{content:i},parts:s}=t,n=document.createTreeWalker(i,I,null,!1);let r=z(s),o=s[r],a=-1,l=0;const d=[];let c=null;for(;n.nextNode();){a++;const t=n.currentNode;for(t.previousSibling===c&&(c=null),e.has(t)&&(d.push(t),null===c&&(c=t)),null!==c&&l++;void 0!==o&&o.index===a;)o.index=null!==c?-1:o.index-l,o=s[r=z(s,r)]}d.forEach(t=>t.parentNode.removeChild(t))}const U=t=>{let e=11===t.nodeType?0:1;const i=document.createTreeWalker(t,I,null,!1);for(;i.nextNode();)e++;return e},z=(t,e=-1)=>{for(let i=e+1;i`${t}--${e}`;let q=!0;void 0===window.ShadyCSS?q=!1:void 0===window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),q=!1);const B=t=>e=>{const i=j(e.type,t);let s=$.get(i);void 0===s&&(s={stringsArray:new WeakMap,keyString:new Map},$.set(i,s));let n=s.stringsArray.get(e.strings);if(void 0!==n)return n;const r=e.strings.join(a);if(void 0===(n=s.keyString.get(r))){const i=e.getTemplateElement();q&&window.ShadyCSS.prepareTemplateDom(i,t),n=new h(e,i),s.keyString.set(r,n)}return s.stringsArray.set(e.strings,n),n},H=["html","svg"],L=new Set,F=(t,e,i)=>{L.add(t);const s=i?i.element:document.createElement("template"),n=e.querySelectorAll("style"),{length:r}=n;if(0===r)return void window.ShadyCSS.prepareTemplateStyles(s,t);const o=document.createElement("style");for(let t=0;t{H.forEach(e=>{const i=$.get(j(e,t));void 0!==i&&i.keyString.forEach(t=>{const{element:{content:e}}=t,i=new Set;Array.from(e.querySelectorAll("style")).forEach(t=>{i.add(t)}),M(t,i)})})})(t);const a=s.content;i?function(t,e,i=null){const{element:{content:s},parts:n}=t;if(null==i)return void s.appendChild(e);const r=document.createTreeWalker(s,I,null,!1);let o=z(n),a=0,l=-1;for(;r.nextNode();)for(l++,r.currentNode===i&&(a=U(e),i.parentNode.insertBefore(e,i));-1!==o&&n[o].index===l;){if(a>0){for(;-1!==o;)n[o].index+=a,o=z(n,o);return}o=z(n,o)}}(i,o,a.firstChild):a.insertBefore(o,a.firstChild),window.ShadyCSS.prepareTemplateStyles(s,t);const l=a.querySelector("style");if(window.ShadyCSS.nativeShadow&&null!==l)e.insertBefore(l.cloneNode(!0),e.firstChild);else if(i){a.insertBefore(o,a.firstChild);const t=new Set;t.add(o),M(i,t)}};window.JSCompiler_renameProperty=((t,e)=>t);const W={toAttribute(t,e){switch(e){case Boolean:return t?"":null;case Object:case Array:return null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){switch(e){case Boolean:return null!==t;case Number:return null===t?null:Number(t);case Object:case Array:return JSON.parse(t)}return t}},D=(t,e)=>e!==t&&(e==e||t==t),J={attribute:!0,type:String,converter:W,reflect:!1,hasChanged:D},Y=Promise.resolve(!0),G=1,X=4,K=8,Q=16,Z=32,tt="finalized";class et extends HTMLElement{constructor(){super(),this._updateState=0,this._instanceProperties=void 0,this._updatePromise=Y,this._hasConnectedResolver=void 0,this._changedProperties=new Map,this._reflectingProperties=void 0,this.initialize()}static get observedAttributes(){this.finalize();const t=[];return this._classProperties.forEach((e,i)=>{const s=this._attributeNameForProperty(i,e);void 0!==s&&(this._attributeToPropertyMap.set(s,i),t.push(s))}),t}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const t=Object.getPrototypeOf(this)._classProperties;void 0!==t&&t.forEach((t,e)=>this._classProperties.set(e,t))}}static createProperty(t,e=J){if(this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t))return;const i="symbol"==typeof t?Symbol():`__${t}`;Object.defineProperty(this.prototype,t,{get(){return this[i]},set(e){const s=this[t];this[i]=e,this._requestUpdate(t,s)},configurable:!0,enumerable:!0})}static finalize(){const t=Object.getPrototypeOf(this);if(t.hasOwnProperty(tt)||t.finalize(),this[tt]=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const t=this.properties,e=[...Object.getOwnPropertyNames(t),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t):[]];for(const i of e)this.createProperty(i,t[i])}}static _attributeNameForProperty(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}static _valueHasChanged(t,e,i=D){return i(t,e)}static _propertyValueFromAttribute(t,e){const i=e.type,s=e.converter||W,n="function"==typeof s?s:s.fromAttribute;return n?n(t,i):t}static _propertyValueToAttribute(t,e){if(void 0===e.reflect)return;const i=e.type,s=e.converter;return(s&&s.toAttribute||W.toAttribute)(t,i)}initialize(){this._saveInstanceProperties(),this._requestUpdate()}_saveInstanceProperties(){this.constructor._classProperties.forEach((t,e)=>{if(this.hasOwnProperty(e)){const t=this[e];delete this[e],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(e,t)}})}_applyInstanceProperties(){this._instanceProperties.forEach((t,e)=>this[e]=t),this._instanceProperties=void 0}connectedCallback(){this._updateState=this._updateState|Z,this._hasConnectedResolver&&(this._hasConnectedResolver(),this._hasConnectedResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(t,e,i){e!==i&&this._attributeToProperty(t,i)}_propertyToAttribute(t,e,i=J){const s=this.constructor,n=s._attributeNameForProperty(t,i);if(void 0!==n){const t=s._propertyValueToAttribute(e,i);if(void 0===t)return;this._updateState=this._updateState|K,null==t?this.removeAttribute(n):this.setAttribute(n,t),this._updateState=this._updateState&~K}}_attributeToProperty(t,e){if(this._updateState&K)return;const i=this.constructor,s=i._attributeToPropertyMap.get(t);if(void 0!==s){const t=i._classProperties.get(s)||J;this._updateState=this._updateState|Q,this[s]=i._propertyValueFromAttribute(e,t),this._updateState=this._updateState&~Q}}_requestUpdate(t,e){let i=!0;if(void 0!==t){const s=this.constructor,n=s._classProperties.get(t)||J;s._valueHasChanged(this[t],e,n.hasChanged)?(this._changedProperties.has(t)||this._changedProperties.set(t,e),!0!==n.reflect||this._updateState&Q||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(t,n))):i=!1}!this._hasRequestedUpdate&&i&&this._enqueueUpdate()}requestUpdate(t,e){return this._requestUpdate(t,e),this.updateComplete}async _enqueueUpdate(){let t,e;this._updateState=this._updateState|X;const i=this._updatePromise;this._updatePromise=new Promise((i,s)=>{t=i,e=s});try{await i}catch(t){}this._hasConnected||await new Promise(t=>this._hasConnectedResolver=t);try{const t=this.performUpdate();null!=t&&await t}catch(t){e(t)}t(!this._hasRequestedUpdate)}get _hasConnected(){return this._updateState&Z}get _hasRequestedUpdate(){return this._updateState&X}get hasUpdated(){return this._updateState&G}performUpdate(){this._instanceProperties&&this._applyInstanceProperties();let t=!1;const e=this._changedProperties;try{(t=this.shouldUpdate(e))&&this.update(e)}catch(e){throw t=!1,e}finally{this._markUpdated()}t&&(this._updateState&G||(this._updateState=this._updateState|G,this.firstUpdated(e)),this.updated(e))}_markUpdated(){this._changedProperties=new Map,this._updateState=this._updateState&~X}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(t){return!0}update(t){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach((t,e)=>this._propertyToAttribute(e,this[e],t)),this._reflectingProperties=void 0)}updated(t){}firstUpdated(t){}}et[tt]=!0;const it=(t,e)=>"method"!==e.kind||!e.descriptor||"value"in e.descriptor?{kind:"field",key:Symbol(),placement:"own",descriptor:{},initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}}:Object.assign({},e,{finisher(i){i.createProperty(e.key,t)}}),st=(t,e,i)=>{e.constructor.createProperty(i,t)};function nt(t){return(e,i)=>void 0!==i?st(t,e,i):it(t,e)}const rt="adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,ot=Symbol();class at{constructor(t,e){if(e!==ot)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return void 0===this._styleSheet&&(rt?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const lt=(t,...e)=>{const i=e.reduce((e,i,s)=>e+(t=>{if(t instanceof at)return t.cssText;if("number"==typeof t)return t;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[s+1],t[0]);return new at(i,ot)};(window.litElementVersions||(window.litElementVersions=[])).push("2.2.1");const dt=t=>t.flat?t.flat(1/0):function t(e,i=[]){for(let s=0,n=e.length;s(t.add(e),t),new Set).forEach(t=>e.unshift(t))}else t&&e.push(t);return e}initialize(){super.initialize(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){const t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?rt?this.renderRoot.adoptedStyleSheets=t.map(t=>t.styleSheet):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map(t=>t.cssText),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){super.update(t);const e=this.render();e instanceof b&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach(t=>{const e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)}))}render(){}}ct.finalized=!0,ct.render=((t,e,i)=>{if(!i||"object"!=typeof i||!i.scopeName)throw new Error("The `scopeName` option is required.");const s=i.scopeName,r=O.has(e),o=q&&11===e.nodeType&&!!e.host,a=o&&!L.has(s),l=a?document.createDocumentFragment():e;if(((t,e,i)=>{let s=O.get(e);void 0===s&&(n(e,e.firstChild),O.set(e,s=new x(Object.assign({templateFactory:k},i))),s.appendInto(e)),s.setValue(t),s.commit()})(t,l,Object.assign({templateFactory:B(s)},i)),a){const t=O.get(l);O.delete(l);const i=t.value instanceof g?t.value.template:void 0;F(s,l,i),n(e,e.firstChild),e.appendChild(l),O.set(e,t)}!r&&o&&window.ShadyCSS.styleElement(e.host)});let ht=lt`rgb(14, 171, 56)`;const pt=lt`
2 | ha-card {
3 | padding: 24px 16px 16px 16px;
4 | background-repeat: no-repeat;
5 | background-size: 100% 100% !important;
6 | }
7 |
8 | .banner {
9 | display: flex;
10 | align-items: flex-end;
11 | background-repeat: no-repeat;
12 | background-size: cover;
13 | background-position: center;
14 | padding-top: 12px;
15 |
16 | background-color: var(--banner-background);
17 | border-radius: 3px;
18 | }
19 |
20 | .has-plant-image .banner {
21 | padding-top: 30%;
22 | }
23 |
24 | .header {
25 | @apply --paper-font-headline;
26 | line-height: 40px;
27 | padding: 8px 16px;
28 | font-weight: 500;
29 | font-size: 125%;
30 | }
31 |
32 | .has-plant-image .header {
33 | font-size: 16px;
34 | font-weight: 500;
35 | line-height: 16px;
36 | padding: 16px;
37 | color: white;
38 | width: 100%;
39 | background: rgba(0, 0, 0, var(--dark-secondary-opacity));
40 | }
41 |
42 | .content {
43 | display: flex;
44 | justify-content: space-between;
45 | padding: 16px 32px 24px 32px;
46 | background-color: var(--content-background);
47 | border-radius: 3px;
48 | }
49 |
50 | .has-plant-image .content {
51 | padding-bottom: 16px;
52 | }
53 |
54 | ha-icon {
55 | color: var(--paper-item-icon-color);
56 | margin-bottom: 8px;
57 | }
58 |
59 | .attributes {
60 | cursor: pointer;
61 | }
62 |
63 | .attributes div {
64 | text-align: center;
65 | }
66 |
67 | .problem {
68 | color: var(--google-red-500);
69 | font-weight: bold;
70 | }
71 |
72 | .uom {
73 | color: var(--secondary-text-color);
74 | }
75 |
76 | .table-tr-td-border-bottom {
77 | border-bottom-color: var(--table-tr-td-border-bottom);
78 | border-bottom: solid 1px;
79 | }
80 |
81 | .div-border-top {
82 | border-top-color: var(--table-tr-td-border-bottom);
83 | border-top: solid 1px;
84 | }
85 |
86 | .div-border-bottom >table {
87 | border-bottom-color: var(--table-tr-td-border-bottom);
88 | border-bottom: solid 1px;
89 | }
90 |
91 | /* CUSTOM */
92 |
93 | table {
94 | width: 100%;
95 | }
96 |
97 | table thead {
98 |
99 | }
100 |
101 | table tr {
102 | border-top: 1px solid black;
103 | }
104 |
105 | table tr:first-child {
106 | border-top: 0;
107 | }
108 |
109 | table tr:first-child {
110 | border-left: 0; border-right: 0;
111 | }
112 |
113 | table tr:last-child {
114 | border-bottom: 0;
115 | }
116 |
117 | table tr:last-child{
118 | border-right: 0;
119 | }
120 |
121 | .fcasttooltip {
122 | position: relative;
123 | display: inline-block;
124 | }
125 |
126 | .fcasttooltip .fcasttooltiptext {
127 | visibility: hidden;
128 |
129 | width: ${400}px;
130 | background-color: ${lt`rgba(50,50,50,0.85)`};
131 | color: ${lt`#fff`};
132 | text-align: center;
133 | border-radius: 6px;
134 | border-style: solid;
135 | border-color: ${ht};
136 | border-width: ${1}px;
137 | padding: 5px 0;
138 | /* Position the tooltip */
139 | position: absolute;
140 | z-index: 1;
141 | top: 100%;
142 | left: 0%;
143 | margin-left: ${-325}px;
144 | }
145 |
146 | .fcasttooltip .fcasttooltiptext:after {
147 | content: "";
148 | position: absolute;
149 | top: 100%;
150 | left: 50%;
151 | margin-left: -${5}px;
152 | border-width: ${5}px;
153 | border-style: solid;
154 | border-color: ${ht} transparent transparent transparent;
155 | }
156 |
157 | .fcasttooltip:hover .fcasttooltiptext {
158 | visibility: ${lt`visible`};
159 | }
160 | `;console.info("%c XIAOMI-MI-FLORA-AND-FLOWER-CARE-CARD %c 1.2.0 ","color: white; background: green; font-weight: 700;","color: coral; background: white; font-weight: 700;");let ut=class extends ct{constructor(){super(...arguments),this.invalidConfig=!1,this.invalidEntity=!1,this.displayInfo=!1,this.displayMaintenance=!1,this.MOINSTURE="moisture",this.CONDUCTIVITY="conductivity",this.BRIGHTNESS="brightness",this.TEMPERATURE="temperature",this.BATTERY="battery"}setConfig(t){if(console.log({flora_care_card_config:t}),!t)throw this.invalidConfig=!0,new Error("Invalid configuration");if(!t.entity||0==t.entity.length)throw this.invalidEntity=!0,new Error("Entity is required");if(t.display&&t.display.length>0){let e=t.display.map(function(t){return t.toLocaleLowerCase()});this.displayMaintenance=e.includes("maintenance"),this.displayInfo=e.includes("info")}this._config=t}getCardSize(){return 1}static get styles(){return pt}render(){return this.invalidConfig||this.invalidEntity?V`
161 |
162 |
163 |
164 |
165 |
166 | Configuration ERROR!
167 |
168 |
169 | `:this._render()}_render(){this._floraCare=this.hass.states[this._config.entity],this._floraRanges=this._floraCare.attributes.ranges;let t=this.computeContent(),e=""!==t.strings.raw.toString(),i=this.displayInfo&&(this.displayMaintenance||e)?"banner div-border-bottom":"banner",s=this.displayMaintenance&&e?"banner div-border-bottom":"banner";return V`
170 |
171 | ${this.computeHeader()}
172 |
173 | ${this.displayInfo?V`
174 | ${this.computeInfoToolTips(this._floraCare.attributes.info)}
175 | `:V``}
176 | ${this.displayMaintenance?V`
177 | ${this.computeMaintenanceToolTips(this._floraCare.attributes.maintenance)}
178 | `:V``}
179 | ${e?V`
180 |
181 | ${t}
182 |
183 | `:V``}
184 |
185 | `}computeContent(){let t=this.getSensor(this._floraCare.attributes.sensors[this.MOINSTURE]),e=this.getSensor(this._floraCare.attributes.sensors[this.CONDUCTIVITY]),i=this.getSensor(this._floraCare.attributes.sensors[this.BRIGHTNESS]),s=this.getSensor(this._floraCare.attributes.sensors[this.TEMPERATURE]);return t||e||i||s?V`
186 | ${t?this.computeContentItem(t,this._floraRanges.min_soil_moist,this._floraRanges.max_soil_moist,this.computeAttributeClass(this._floraCare.attributes.problem,t,this.MOINSTURE),"mdi:water-percent"):V``}
187 | ${e?this.computeContentItem(e,this._floraRanges.min_soil_ec,this._floraRanges.max_soil_ec,this.computeAttributeClass(this._floraCare.attributes.problem,e,this.CONDUCTIVITY),"mdi:flash-circle"):V``}
188 | ${i?this.computeContentItem(i,this._floraRanges.min_light_lux,this._floraRanges.max_light_lux,this.computeAttributeClass(this._floraCare.attributes.problem,i,this.BRIGHTNESS),"mdi:white-balance-sunny"):V``}
189 | ${s?this.computeContentItem(s,this._floraRanges.min_temp,this._floraRanges.max_temp,this.computeAttributeClass(this._floraCare.attributes.problem,s,this.TEMPERATURE),"mdi:thermometer"):V``}
190 | `:V``}computeTitle(){return this._config&&this._config.name}computeContentItem(t,e,i,s,n){if(void 0!==t){let r=t.attributes&&t.attributes.icon?t.attributes.icon:n;return V`
191 | this.handlePopup(e,t.entity_id)}>
192 |
193 |
194 |
195 |
196 | ${-1===t.state.indexOf("unknown")?t.state+" "+t.attributes.unit_of_measurement:"n/a"}
197 |
198 |
199 | ${null!==e&&null!==i?V`${e}-${i}`:V``}
200 |
201 |
202 | `}return V`
203 | `}computeMaintenanceToolTips(t){return V`
204 |
205 |
206 | | Maintenance |
207 |
208 |
209 | ${Object.keys(t).map(e=>V`
210 |
211 | | ${this.capitalize(e)} |
212 | ${t[e]} |
213 |
214 | `)}
215 |
216 |
217 | `}computeInfoToolTips(t){return V`
218 |
219 |
220 | ${Object.keys(t).filter(t=>"floral_language"!=t).map(e=>V`
221 |
222 | | ${this.capitalize(e)} |
223 | ${t[e]} |
224 |
225 | `)}
226 |
227 |
228 | `}computeHeader(){let t=this.getSensor(this._floraCare.attributes.sensors[this.BATTERY]);return V`
229 |
230 |
231 |
232 |
233 | |
234 |
242 | |
243 |
244 |
252 | |
253 |
254 | ${this.computeContentItem(t,null,null,this.computeAttributeClass(this._floraCare.attributes.problem,t,this.BATTERY),"mdi:battery-charging")}
255 | |
256 |
257 |
258 |
259 |
260 |
261 |
262 | `}computeAttributeClass(t,e,i){return-1===t.indexOf(i)&&void 0!==e&&-1===e.state.indexOf("unknown")?"":"problem"}getSensor(t){return this.hass.states[t]}capitalize(t){return t.charAt(0).toUpperCase()+t.slice(1)}handlePopup(t,e){t.stopPropagation();let i=new Event("hass-more-info",{composed:!0});i.detail={entityId:e},this.dispatchEvent(i)}};t([nt()],ut.prototype,"hass",void 0),t([nt()],ut.prototype,"_config",void 0),t([nt()],ut.prototype,"_floraCare",void 0),t([nt()],ut.prototype,"_floraRanges",void 0),ut=t([(t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:s}=e;return{kind:i,elements:s,finisher(e){window.customElements.define(t,e)}}})(t,e))("xiaomi-mi-flora-and-flower-care-card")],ut);
263 |
--------------------------------------------------------------------------------