智能家居笔记Home-Assistant+小智AI

智能家居笔记Home-Assistant+小智AI

设备框架图

概述: 总体而言借用各种开源项目,致力做到好用可控。输入设备小智AI作为用户前端,后端用小智AI华南理工开源服务器。智能家庭中控采用树莓派5搭载HAOS,用homeassistant,包括手机APP。各种终端设备,支持zigbee通信协议,wifi通信协议,小米设备支持milot的设备可以连接,其他类似美的海尔的设备也看home assistant的插件支持程度,没有本身开源的使用舒适。

image.png

主机: thinkpad-S5-yoga 地址xxxxxxxxxxxxx 用户:xxxxxxxxxxx

服务备注服务端口
frigate5000
sambamnt/media/usbshare
http/mnt/usb_share/podcast10086
mediamtx.servicexxxxxxxxxxx8554
napcat
koi
miloco
micamxxxxxxxxxxxxxx
astra-color.service
zaokafei-fetch.timer/mnt/usb_share/podcast/zaokafei
bambucam.serviceDesktop/bambustudio/bambucam5004
glances.service61208

主机: MacBookAir 地址:xxxxxxxxxxxxx 用户:xxxxxxxxxxxxx

服务备注服务端口
intel-color
ir
switchDesktop/swtich_rtsp/swtich_rtsp.py
mediamtxxxxxxxxxxxxxx8554

主机: PC 地址:xxxxxxxxxxxxxxx 用户:xxxxxxxxxxx

image.png

小智ESP32嵌入式前端

开源地址:78/xiaozhi-esp32: An MCP-based chatbot | 一个基于MCP的聊天机器人

前端若需要连M3.5的扬声器接口,可换PCM5102A模块

参考链接:用 ESP32 + PCM5102 打造一个无线的HIFI音乐播放器-ZEEKLOG博客

重新配置OTA地址,开机时按boot键

小智ESP32后端服务器

开源地址:xinnan-tech/xiaozhi-esp32-server: 本项目为xiaozhi-esp32提供后端服务,帮助您快速搭建ESP32设备控制服务器。Backend service for xiaozhi-esp32, helps you quickly build an ESP32 device control server.

连接home assistant采用了很多尝试
最先MCP连接方式: ha-mcp-for-xiaozhi/README.md at v0.1.1 · c1pher-cn/ha-mcp-for-xiaozhi
该方式能将多个SSE都连入Home assistant 封装为 function call 让xiaozhi-server调用,在home assistant端采用的是assist提供的接口

也尝试用 xiaozhi-server贡献的home assistant代码,其实用了home assisttant的API,好处是可以精简MCP,对音乐播放进行了一些修改。

Home Assistant

树莓派地址:xxxxxxxxxxxxxx

公网访问

image.png

使用frp 加载项 内网穿透至阿里云服务器,再nginx反向代理出来。
参考:Frp内网穿透使用记录 - kfzzzzzz_blog

感觉有一个bug,重启HAOS之后,自启动FRP Clinet,但是FRP Server会还在重新尝试原先的客户端,就会崩溃。因此必须开崩溃自动恢复。

网络存储

此处用部署在局域网的samba服务,用户名:kfzzzzzz

image.png

Music Assistant

使用provider模式,youtube music等使用延迟较大,因此还是本地使用方案。

歌曲下载地址

SQUID.WTF - Home
lucida | music at internet speed

使用 Filesystem provider

image.png

podcast收听地址

发现RSS真的还挺方便

使用podcast provider

image.png

链接:weekend-project-space/top-rss-list: 订阅人数最多的rss源,中文优质rss源

也可以自己抓取节目,小宇宙的音频都可以抓比较方便

抓取代码Desktop/zaokafei

#!/usr/bin/env python3 # -*- coding: utf-8 -*- import json import os import re from datetime import datetime, timezone from email.utils import format_datetime from urllib.parse import quote import requests import xml.etree.ElementTree as ET PODCAST_URL = "https://www.xiaoyuzhoufm.com/podcast/60de7c003dd577b40d5a40f3" # 音频/封面/XML 都放这里(U 盘) OUT_DIR = "/mnt/usb_share/podcast/zaokafei" XML_PATH = os.path.join(OUT_DIR, "zaokafei.xml") # 你这台 ThinkPad 的 HTTP 服务地址(用你确认的 IP) BASE_HTTP = "http://192.168.31.75:10086" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", } def get_next_data(url: str) -> dict: """从小宇宙网页里解析 __NEXT_DATA__ JSON""" html = requests.get(url, headers=HEADERS, timeout=30).text m = re.search( r'<script type="application/json">(.*?)</script>', html, re.S, ) if not m: raise RuntimeError("没找到 __NEXT_DATA__,小宇宙页面结构可能变了") return json.loads(m.group(1)) def sanitize_filename(name: str) -> str: """文件名安全化(保留中文,但去掉不允许字符,避免太长)""" name = re.sub(r'[\\/:*?"<>|]', "_", name).strip() name = re.sub(r"\s+", " ", name) return name[:160] if len(name) > 160 else name def parse_pubdate_to_utc(pub: str) -> datetime: """ 解析小宇宙 pubDate(通常 ISO8601,可能带 Z) 返回 timezone-aware UTC datetime """ try: if pub.endswith("Z"): dt = datetime.fromisoformat(pub.replace("Z", "+00:00")) else: dt = datetime.fromisoformat(pub) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(timezone.utc) except Exception: # 解析失败就用当前时间兜底(不影响下载,只影响 pubDate) return datetime.now(timezone.utc) def download_file(url: str, path: str): """流式下载,先写 .part 再原子替换""" r = requests.get(url, headers=HEADERS, stream=True, timeout=180) r.raise_for_status() tmp = path + ".part" with open(tmp, "wb") as f: for chunk in r.iter_content(chunk_size=1024 * 256): if chunk: f.write(chunk) os.replace(tmp, path) def ensure_xml_exists(): if os.path.exists(XML_PATH): return raise RuntimeError( f"找不到 {XML_PATH}\n" f"请先把 zaokafei.xml 放到 {OUT_DIR} 下(至少包含 <rss><channel>...</channel></rss>)" ) def xml_has_guid(root: ET.Element, guid: str) -> bool: for item in root.findall("./channel/item"): g = item.findtext("guid") if g and g.strip() == guid: return True return False def add_item_to_xml( title: str, guid: str, pub_dt_utc: datetime, enclosure_url: str, length_bytes: int, ): """ 把新一期插入到 XML 的最前面(靠前显示) 仅写入最必要字段:title/pubDate/guid/enclosure """ ET.register_namespace("itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd") tree = ET.parse(XML_PATH) root = tree.getroot() channel = root.find("channel") if channel is None: raise RuntimeError("XML 里没找到 <channel>") if xml_has_guid(root, guid): print("XML 已存在该期 guid,跳过写入:", guid) return item = ET.Element("item") t = ET.SubElement(item, "title") t.text = title pub = ET.SubElement(item, "pubDate") pub.text = format_datetime(pub_dt_utc) # RFC 2822 g = ET.SubElement(item, "guid", attrib={"isPermaLink": "false"}) g.text = guid ET.SubElement( item, "enclosure", attrib={ "url": enclosure_url, "length": str(length_bytes), "type": "audio/mp4", }, ) # 插到最前面:放在第一个 item 前 first_item = channel.find("item") if first_item is None: channel.append(item) else: children = list(channel) idx = children.index(first_item) channel.insert(idx, item) tree.write(XML_PATH, encoding="UTF-8", xml_declaration=True) print("已更新 XML:", XML_PATH) def main(): os.makedirs(OUT_DIR, exist_ok=True) ensure_xml_exists() data = get_next_data(PODCAST_URL) podcast = data["props"]["pageProps"]["podcast"] latest = podcast["episodes"][0] eid = latest["eid"] title = latest["title"] pub_raw = latest.get("pubDate", "") # 音频 URL 兼容两种字段 audio_url = ( latest.get("media", {}).get("source", {}).get("url") or latest.get("enclosure", {}).get("url") ) if not audio_url: raise RuntimeError("没找到 media.source.url / enclosure.url") pub_dt_utc = parse_pubdate_to_utc(pub_raw) date_str = pub_dt_utc.strftime("%Y-%m-%d") safe_title = sanitize_filename(title) filename = f"{date_str} - {safe_title}.m4a" out_path = os.path.join(OUT_DIR, filename) # 先判断 XML 是否已经有 guid:有的话可以直接结束(更省事) # 但为了保险(避免 XML 手动改动),我们仍然会检查文件是否存在。 print("最新一期:", title) print("EID:", eid) print("发布时间(UTC):", pub_dt_utc.isoformat()) print("音频:", audio_url) if os.path.exists(out_path): print("文件已存在,跳过下载:", out_path) else: download_file(audio_url, out_path) print("下载完成:", out_path) # URL 编码文件名(中文/空格更稳) enclosure_url = f"{BASE_HTTP}{quote('/' + filename)}" length_bytes = os.path.getsize(out_path) add_item_to_xml( title=title, guid=eid, pub_dt_utc=pub_dt_utc, enclosure_url=enclosure_url, length_bytes=length_bytes, ) if __name__ == "__main__": main() 

然后放一个xml文件

<?xml version='1.0' encoding='UTF-8'?> <rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0"> <channel> <title>声动早咖啡</title> <link>http://192.168.31.75:10086/</link> <description> 一个十五分钟的晨间仪式,轻松同步日常生活与商业世界。 工作日早晨更新,来自声动活泼的清晨播客。 </description> <itunes:image href="http://192.168.31.75:10086/cover.png" /> <image> <url>http://192.168.31.75:10086/cover.png</url> <title>声动早咖啡</title> <link>http://192.168.31.75:10086/</link> </image> <item><title>咖啡豆|18 世纪就被誉为「全球最好」的美利奴羊毛,为何最近在国内热度大增?</title><pubDate>Thu, 08 Jan 2026 23:00:00 +0000</pubDate><guid isPermaLink="false">695fce90d4b8fa56f5f949fe</guid><enclosure url="http://192.168.31.75:10086/2026-01-08%20-%20%E5%92%96%E5%95%A1%E8%B1%86%EF%BD%9C18%20%E4%B8%96%E7%BA%AA%E5%B0%B1%E8%A2%AB%E8%AA%89%E4%B8%BA%E3%80%8C%E5%85%A8%E7%90%83%E6%9C%80%E5%A5%BD%E3%80%8D%E7%9A%84%E7%BE%8E%E5%88%A9%E5%A5%B4%E7%BE%8A%E6%AF%9B%EF%BC%8C%E4%B8%BA%E4%BD%95%E6%9C%80%E8%BF%91%E5%9C%A8%E5%9B%BD%E5%86%85%E7%83%AD%E5%BA%A6%E5%A4%A7%E5%A2%9E%EF%BC%9F.m4a" length="14036410" type="audio/mp4" /></item><item><title>图拉斯|必胜客推出独立汉堡门店,外卖大战补贴降温</title><pubDate>Wed, 07 Jan 2026 23:00:00 +0000</pubDate><guid isPermaLink="false">695e474fc1e012a7abdcfb1e</guid><enclosure url="http://192.168.31.75:10086/2026-01-07%20-%20%E5%9B%BE%E6%8B%89%E6%96%AF%EF%BD%9C%E5%BF%85%E8%83%9C%E5%AE%A2%E6%8E%A8%E5%87%BA%E7%8B%AC%E7%AB%8B%E6%B1%89%E5%A0%A1%E9%97%A8%E5%BA%97%EF%BC%8C%E5%A4%96%E5%8D%96%E5%A4%A7%E6%88%98%E8%A1%A5%E8%B4%B4%E9%99%8D%E6%B8%A9.m4a" length="16226181" type="audio/mp4" /></item><item><title>没有鱼子酱饮食传统的中国,为何成为全球鱼子酱的主要产地?</title><pubDate>Tue, 06 Jan 2026 23:00:00 +0000</pubDate><guid isPermaLink="false">695d07efc1e012a7abad496a</guid><enclosure url="http://192.168.31.75:10086/2026-01-06%20-%20%E6%B2%A1%E6%9C%89%E9%B1%BC%E5%AD%90%E9%85%B1%E9%A5%AE%E9%A3%9F%E4%BC%A0%E7%BB%9F%E7%9A%84%E4%B8%AD%E5%9B%BD%EF%BC%8C%E4%B8%BA%E4%BD%95%E6%88%90%E4%B8%BA%E5%85%A8%E7%90%83%E9%B1%BC%E5%AD%90%E9%85%B1%E7%9A%84%E4%B8%BB%E8%A6%81%E4%BA%A7%E5%9C%B0%EF%BC%9F.m4a" length="14320957" type="audio/mp4" /></item><item><title>山姆中国付费会员首次破千万,泡泡玛特将加速海外扩张</title><pubDate>Mon, 05 Jan 2026 23:00:00 +0000</pubDate><guid isPermaLink="false">695bafd9bdbeb6a09ee00719</guid><enclosure url="http://192.168.31.75:10086/2026-01-05%20-%20%E5%B1%B1%E5%A7%86%E4%B8%AD%E5%9B%BD%E4%BB%98%E8%B4%B9%E4%BC%9A%E5%91%98%E9%A6%96%E6%AC%A1%E7%A0%B4%E5%8D%83%E4%B8%87%EF%BC%8C%E6%B3%A1%E6%B3%A1%E7%8E%9B%E7%89%B9%E5%B0%86%E5%8A%A0%E9%80%9F%E6%B5%B7%E5%A4%96%E6%89%A9%E5%BC%A0.m4a" length="11324711" type="audio/mp4" /></item><item><title>频繁发射火箭的 SpaceX 今年上市,为什么星链服务是其价值关键?</title><pubDate>Sun, 04 Jan 2026 23:00:00 +0000</pubDate><guid isPermaLink="false">695a8775b9fb62614108f66e</guid><enclosure url="http://192.168.31.75:10086/2026-01-04%20-%20%E9%A2%91%E7%B9%81%E5%8F%91%E5%B0%84%E7%81%AB%E7%AE%AD%E7%9A%84%20SpaceX%20%E4%BB%8A%E5%B9%B4%E4%B8%8A%E5%B8%82%EF%BC%8C%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%9F%E9%93%BE%E6%9C%8D%E5%8A%A1%E6%98%AF%E5%85%B6%E4%BB%B7%E5%80%BC%E5%85%B3%E9%94%AE%EF%BC%9F.m4a" length="15438817" type="audio/mp4" /></item></channel> 

把自己的文件夹地址代理出来就可以

Frigate

摄像头管理NVR,目前我是跑在I5-5500U,M840上,其实无法硬解H265视频,并且做detect其实也比较吃力。
官方地址:Frigate中文文档 - Frigate 中文文档

视频硬解适配

image.png

intel cpu:

image.png

nvidia:Video Encode and Decode Support Matrix | NVIDIA Developer

在我的配置中,用vappi对H264硬解,对H265软解

hwaccel_args: preset-vaapi 

有时候会报错,需要指定设备及视频格式

 hwaccel_args: - -hwaccel - vaapi - -hwaccel_device - /dev/dri/renderD128 - -hwaccel_output_format - yuv420p 

视频推流

rtsp推流,但是用qsv进行硬编码。

各种嵌入式设备

Marshall音箱

因为该音箱是一个有源音箱,连接方式有蓝牙和3.5mm连接两种

music assistant 安装 snapcast provider

image.png

在终端设备安装snapcast 客户端

甲醛检测仪

使用ZH08 CH2O传感器,是电化学传感器

image.png
image.png

具体代码

ze08-ch2o.yaml

esphome: name: ze08-ch2o friendly_name: ZE08 CH2O esp32: board: esp32dev framework: type: arduino wifi: ssid: "xxxxxxxxxxxxx" password: "xxxxxxxxxxx" logger: baud_rate: 0 api: ota: - platform: esphome external_components: - source: type: local path: components components: [ ze08_ch2o ] uart: id: uart_ze08 rx_pin: GPIO3 # 板子上 RX tx_pin: GPIO1 # 板子上 TX baud_rate: 9600 data_bits: 8 parity: NONE stop_bits: 1 ze08_ch2o: id: ze08_sensor uart_id: uart_ze08 ch2o_ppb: name: "CH2O (ppb)" ch2o_mg_m3: name: "CH2O (mg/m³)" sensor: - platform: template name: dummy_sensor id: dummy_sensor lambda: |- return 0.0; update_interval: 1h internal: true 

init.py

import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart, sensor from esphome.const import CONF_ID ze08_ns = cg.esphome_ns.namespace("ze08_ch2o") ZE08CH2OUart = ze08_ns.class_("ZE08CH2OUart", cg.Component, uart.UARTDevice) CONF_PPB = "ch2o_ppb" CONF_MGM3 = "ch2o_mg_m3" CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ZE08CH2OUart), cv.Required(CONF_PPB): sensor.sensor_schema( unit_of_measurement="ppb", accuracy_decimals=0, icon="mdi:molecule", ), cv.Required(CONF_MGM3): sensor.sensor_schema( unit_of_measurement="mg/m³", accuracy_decimals=3, icon="mdi:molecule", ), } ).extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) ppb = await sensor.new_sensor(config[CONF_PPB]) mgm3 = await sensor.new_sensor(config[CONF_MGM3]) cg.add(var.set_sensors(ppb, mgm3)) 

ze08_ch2o_uart.h

#pragma once #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" namespace esphome { namespace ze08_ch2o { class ZE08CH2OUart : public Component, public uart::UARTDevice { public: ZE08CH2OUart() : uart::UARTDevice(nullptr) {} explicit ZE08CH2OUart(uart::UARTComponent *parent) : uart::UARTDevice(parent) {} void set_sensors(sensor::Sensor *ppb, sensor::Sensor *mgm3) { ch2o_ppb_ = ppb; ch2o_mg_m3_ = mgm3; } void loop() override { while (available()) { uint8_t b = read(); if (idx_ == 0) { if (b != 0xFF) continue; buf_[idx_++] = b; continue; } buf_[idx_++] = b; if (idx_ >= 9) { parse_frame_(); idx_ = 0; } } } protected: sensor::Sensor *ch2o_ppb_{nullptr}; sensor::Sensor *ch2o_mg_m3_{nullptr}; uint8_t buf_[9]{0}; uint8_t idx_{0}; void parse_frame_() { // FF 17 04 dec hi lo fs_hi fs_lo checksum if (buf_[0] != 0xFF) return; if (buf_[1] != 0x17) return; // CH2O if (buf_[2] != 0x04) return; // ppb uint16_t sum = 0; for (int i = 1; i <= 7; i++) sum += buf_[i]; uint8_t cs = (uint8_t)(~(sum & 0xFF) + 1); if (cs != buf_[8]) return; uint16_t ppb = ((uint16_t) buf_[4] << 8) | buf_[5]; float mgm3 = (ppb / 1000.0f) * 1.25f; if (ch2o_ppb_) ch2o_ppb_->publish_state(ppb); if (ch2o_mg_m3_) ch2o_mg_m3_->publish_state(mgm3); } }; } // namespace ze08_ch2o } // namespace esphome 

刷机页面

链接:Web - ESPHome

IntelRealsense摄像头/astra_color摄像头

采用qsv硬编码

image.png

夜视模式根据灰度直方图判断自动切换切换夜里用IR流和RGB流,同一个流输出

image.png

小米4C摄像头

由于小米摄像头本身无提供RTSP视频流,使用小米官方开源的miloco项目接受websocket流,之后三方的micam将此流转换为rtsp视频流,官方用go2rtc推流,我这边改用了mediamtx,另外小米4C为H265编码,docker运行

miloco

开源地址:XiaoMi/xiaomi-miloco: Xiaomi Miloco

micam

开源地址:miiot/micam: 🎦 Micam 是一个专为小米摄像头设计的 RTSP 桥接服务(非官方),能够将小米摄像头的视频流本地转推到RTSP服务器,支持接入 HomeAssistant、Go2rtc、Frigate、Scrypted、Homekit 等多种NVR和智能家居系统。该项目采用 Docker Compose 快速部署方案,基于小米官方的Miloco,并集成Go2rtc实现RTSP流服务,无需GPU即可运行,使小米摄像头能与各类主流智能家居平台无缝集成。

拓竹A1 mini

使用插件:Integration Overview

根据官方配置,能进行的操作似乎蛮多

开启局域网模式以后获取视频流方法

链接:jtessler/bambucam: Linux command line tool to access camera feed from a Bambu 3D printer

但我是A1 mini 似乎智能获取HTTP视频流,效果如下

image.png

ESPHOME排插/MQTT插座/zigbee插座

淘宝购入zigbee信号接收器插在跑HA的树莓派上

0db72f02e4774d804f444f6d128e07b1.jpg

在homeassistant/configuration.yaml 配置

image.png

终端设备淘宝非常多,几种通信协议的设备都能够支持,参考店家说明配置没啥难度

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


Tasmota设置Set’Option 为OFF时为switch,设置为ON时为light

Read more

【PyTorch】2024保姆级安装教程-Python-(CPU+GPU详细完整版)-

【PyTorch】2024保姆级安装教程-Python-(CPU+GPU详细完整版)-

一、准备工作 1. pytorch需要python3.6及以上的python版本 2. 我是利用Anaconda来管理我的python。可自行安装Anaconda。 3. Anaconda官网 Free Download | Anaconda 具体Anaconda安装教程可参考 https://blog.ZEEKLOG.net/weixin_43412762/article/details/129599741?fromshare=blogdetail&sharetype=blogdetail&sharerId=129599741&sharerefer=PC&sharesource=2201_75436278&sharefrom=from_link 二、pytorch介绍 安装 PyTorch 时,可以选择在 CPU 或

By Ne0inhk

Python从0到100完整学习指南(必看导航)

Python 从 0 到 100 完整学习路线(2025–2026 实用版) 这是一条目前在中文社区被验证最多次、性价比最高、就业/副业/考研/转行都适用的 Python 学习路径。 分为 8 个大阶段,每个阶段给出: * 核心目标 * 推荐学习时长(每天 2–4 小时估算) * 最值得学的资源(2025–2026 仍活跃且评价最高的) * 必须掌握的技能清单 * 阶段性小目标 / 实战项目建议 阶段划分总览表 阶段名称目标人群建议时长累计总时长核心关键词0准备期完全零基础3–7 天1 周环境、IDE、学习心态1Python 基础语法零基础 → 能写小工具3–6 周1–2 个月变量、循环、函数、类2Pythonic

By Ne0inhk
Python(31)PyPy生成器优化深度解析:JIT加速下的Python性能革命

Python(31)PyPy生成器优化深度解析:JIT加速下的Python性能革命

目录 * 引言:当生成器遇上JIT编译器 * 一、PyPy生成器核心机制解析 * 1.1 核心机制 * 1.2 字节码层面的革命性优化 * 1.3 JIT编译的三大阶段 * 二、生成器优化策略深度剖析 * 2.1 基础优化策略 * 2.2 高级优化技术 * 2.3 评估与调优 * 2.4 延迟计算的极致优化代码 * 2.5 生成器状态机的智能压缩代码 * 三、生成器性能优化实战案例 * 3.1 蒙特卡洛模拟加速 * 3.2 大数据流处理管道 * 3.3 递归生成器的尾调用优化 * 四、生成器与PyPy的深度整合 * 4.1 协程通信优化 * 4.2 数值计算生成器优化

By Ne0inhk
【Python】【数据分析】Python 数据分析与可视化:全面指南

【Python】【数据分析】Python 数据分析与可视化:全面指南

目录 * 1. 环境准备 * 2. 数据处理与清洗 * 2.1 导入数据 * 2.2 数据清洗 * 示例:处理缺失值 * 示例:处理异常值 * 2.3 数据转换 * 3. 数据分析 * 3.1 描述性统计 * 3.2 分组分析 * 示例:按年龄分组计算工资的平均值 * 3.3 时间序列分析 * 4. 数据可视化 * 4.1 基本绘图 * 示例:柱状图 * 4.2 使用 Seaborn 绘制图表 * 示例:箱型图 * 4.3 高级可视化技巧 * 示例:热力图

By Ne0inhk