├── 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 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) 7 | 8 | [![License][license-shield]](LICENSE) 9 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/r-renato/hass-xiaomi-mi-flora-and-flower-care.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/r-renato/hass-xiaomi-mi-flora-and-flower-care/alerts/) 10 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/r-renato/hass-xiaomi-mi-flora-and-flower-care.svg?logo=lgtm&logoWidth=18)](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 | Home Assistant lovelace card 24 | Home Assistant lovelace card 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 | Home Assistant plant panel 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 | Home Assistant lovelace card 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 | Home Assistant lovelace card 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 | Home Assistant lovelace card 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 | Home Assistant lovelace card 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 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) 7 | 8 | [![License][license-shield]](LICENSE) 9 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/r-renato/hass-xiaomi-mi-flora-and-flower-care.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/r-renato/hass-xiaomi-mi-flora-and-flower-care/alerts/) 10 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/r-renato/hass-xiaomi-mi-flora-and-flower-care.svg?logo=lgtm&logoWidth=18)](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 | Home Assistant lovelace card 24 | Home Assistant lovelace card 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 | Home Assistant plant panel 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 | Home Assistant lovelace card 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 | Home Assistant lovelace card 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 | Home Assistant lovelace card 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 | Home Assistant lovelace card 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 | 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 | 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 | 229 | 230 | 231 | ${Object.keys(maintenance).map( key => html` 232 | 233 | 234 | 235 | 236 | `)} 237 | 238 |
Maintenance
${this.capitalize(key)}${maintenance[key]}
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 | 254 | 255 | 256 | `)} 257 | 258 |
${this.capitalize(key)}${info[key]}
259 | `; 260 | } 261 | 262 | computeHeader() { 263 | let batterySensor = this.getSensor( this._floraCare.attributes.sensors[ this.BATTERY ]) ; 264 | return html` 265 | 266 | 267 | 268 | 269 | 279 | 289 | 295 | 296 | 297 | 298 | 299 | 300 |
${this.computeTitle()} 270 |
271 |
272 | 273 |
${this.computeInfoToolTips(this._floraCare.attributes.info)}
274 |
275 |
276 |
 
277 |
278 |
280 |
281 |
282 | 283 |
${this.computeMaintenanceToolTips(this._floraCare.attributes.maintenance)}
284 |
285 |
286 |
 
287 |
288 |
290 | ${this.computeContentItem(batterySensor, null, null, 291 | this.computeAttributeClass( this._floraCare.attributes.problem, batterySensor, this.BATTERY ), 292 | 'mdi:battery-charging' 293 | )} 294 |
${this._config.zone_name}
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 | 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 | 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 | 207 | 208 | 209 | ${Object.keys(t).map(e=>V` 210 | 211 | 212 | 213 | 214 | `)} 215 | 216 |
Maintenance
${this.capitalize(e)}${t[e]}
217 | `}computeInfoToolTips(t){return V` 218 | 219 | 220 | ${Object.keys(t).filter(t=>"floral_language"!=t).map(e=>V` 221 | 222 | 223 | 224 | 225 | `)} 226 | 227 |
${this.capitalize(e)}${t[e]}
228 | `}computeHeader(){let t=this.getSensor(this._floraCare.attributes.sensors[this.BATTERY]);return V` 229 | 230 | 231 | 232 | 233 | 243 | 253 | 256 | 257 | 258 | 259 | 260 | 261 |
${this.computeTitle()} 234 |
235 |
236 | 237 |
${this.computeInfoToolTips(this._floraCare.attributes.info)}
238 |
239 |
240 |
 
241 |
242 |
244 |
245 |
246 | 247 |
${this.computeMaintenanceToolTips(this._floraCare.attributes.maintenance)}
248 |
249 |
250 |
 
251 |
252 |
254 | ${this.computeContentItem(t,null,null,this.computeAttributeClass(this._floraCare.attributes.problem,t,this.BATTERY),"mdi:battery-charging")} 255 |
${this._config.zone_name}
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 | --------------------------------------------------------------------------------