每次我收到水电费时,我都会感到惊讶——我的家人真的消耗这么多吗? 嗯,是的,浴室有加热地板和锅炉,但它们不会一直生火。 我们似乎也在节约用水(尽管我们也喜欢在浴室里泼水)。 几年前我就已经
我最近改用家庭助理作为我的智能家居系统。 原因之一正是有机会组织大量数据的收集,并能够方便地构建各种类型的图表。
本文中描述的信息并不新鲜;所有这些不同酱料下的东西都已经在互联网上描述过。 但每篇文章通常只描述一种方法或方面。 我必须比较所有这些方法并自己选择最合适的一种。 这篇文章仍然没有提供有关数据收集的全面信息,而是对我如何做到这一点的一种总结。 因此,欢迎提出建设性批评和改进建议。
制定问题
因此,今天练习的目标是获得漂亮的水和电消耗图表:
- 每小时一次,持续 2 天
- 每日一次,持续 2 周
- (可选)每周和每月
这样做有一些困难:
- 标准图表组件通常很差。 充其量,您可以逐点构建折线图。
如果您足够仔细,您可以找到扩展标准图表功能的第三方组件。 对于家庭助理来说,原则上来说,这是一个又好又漂亮的组件
迷你图卡 ,但也有一定的局限性:- 很难在大间隔内设置条形图的参数(条形的宽度以小时的小数形式设置,这意味着超过一小时的间隔将以小数形式设置)
- 您不能将不同的实体添加到一张图表中(例如,温度和湿度,或将条形图与线条组合)
- 家庭助手不仅默认使用最原始的SQLite数据库(而我这个勤杂工无法安装MySQL或Postgres),而且数据也不是以最优化的方式存储的。 因此,例如,每次更改参数中的最小数字参数时,都会将大约 KB 大小的巨大 json 写入数据库
{"entity_id": "sensor.water_cold_hourly", "old_state": {"entity_id": "sensor.water_cold_hourly", "state": "3", "attributes": {"source": "sensor.water_meter_cold", "status": "collecting", "last_period": "29", "last_reset": "2020-02-23T21:00:00.022246+02:00", "meter_period": "hourly", "unit_of_measurement": "l", "friendly_name": "water_cold_hourly", "icon": "mdi:counter"}, "last_changed": "2020-02-23T19:05:06.897604+00:00", "last_updated": "2020-02-23T19:05:06.897604+00:00", "context": {"id": "aafc8ca305ba4e49ad4c97f0eddd8893", "parent_id": null, "user_id": null}}, "new_state": {"entity_id": "sensor.water_cold_hourly", "state": "4", "attributes": {"source": "sensor.water_meter_cold", "status": "collecting", "last_period": "29", "last_reset": "2020-02-23T21:00:00.022246+02:00", "meter_period": "hourly", "unit_of_measurement": "l", "friendly_name": "water_cold_hourly", "icon": "mdi:counter"}, "last_changed": "2020-02-23T19:11:11.251545+00:00", "last_updated": "2020-02-23T19:11:11.251545+00:00", "context": {"id": "0de64b8af6f14bb9a419dcf3b200ef56", "parent_id": null, "user_id": null}}}
我有相当多的传感器(每个房间的温度传感器、水表和电表),有些还产生相当多的数据。 例如,光是SDM220电表每10-15秒就会产生大约十几个值,我想安装大约8个这样的电表,还有一大堆参数是根据其他传感器计算出来的。 那。 所有这些值每天都很容易使数据库膨胀 100-200 MB。 一周后系统几乎不会移动,一个月后闪存驱动器就会失效(在 Raspberry PI 上安装典型家庭助理的情况下),并且存储一整年的数据是不可能的。
- 如果幸运的话,您的电表可以自行计算消耗量。 您可以随时查看计价器,询问累计消费值是几点。 通常,所有具有数字接口(RS232/RS485/Modbus/Zigbee)的电表都提供这种机会。
如果设备可以简单地测量某些瞬时参数(例如瞬时功率或电流),或者只是每 X 瓦时或升生成脉冲,情况会更糟。 然后你需要考虑如何集成、与什么集成以及在哪里积累价值。 无论出于何种原因,都存在错过下一份报告的风险,而且整个系统的准确性也受到质疑。 当然,您可以将所有这些委托给像家庭助理这样的智能家居系统,但是没有人取消关于数据库中记录数量的观点,并且每秒轮询传感器的次数不可能超过一次(家庭助理架构的限制)。
方法1
首先,让我们看看家庭助理提供了哪些开箱即用的功能。 测量一段时间内的消耗量是一项非常受欢迎的功能。 当然,它很早以前就以专门的组件 - utility_meter 的形式实现了。
该组件的本质是它在内部创建一个变量 current_accumulated_value 并在指定时间段(小时/周/月)后重置它。 组件本身监视输入变量(某些传感器的值),订阅值的变化 - 您只需得到最终结果。 这个东西在配置文件里只用几行就描述了
utility_meter:
water_cold_hour_um:
source: sensor.water_meter_cold
cycle: hourly
water_cold_day_um:
source: sensor.water_meter_cold
cycle: daily
这里的sensor.water_meter_cold是我收到的当前仪表值(以升为单位)
lovelace-UI 的每小时和每日图表的代码如下所示:
- type: history-graph
title: 'Hourly water consumption using vars'
hours_to_show: 48
entities:
- sensor.water_hour
- type: history-graph
title: 'Daily water consumption using vars'
hours_to_show: 360
entities:
- sensor.water_day
其实这个方法的问题就出在这个算法上。 正如我已经提到的,对于每个输入值(下一升的当前仪表读数),数据库中都会生成 1kb 的记录。 每个公用事业仪表还会生成一个新值,该值也会添加到基础中。 如果我想收集每小时/每天/每周/每月的读数,以及几个立管的读数,并添加一组电表,这将是大量数据。 嗯,更准确地说,数据并不多,但是由于家庭助理将一堆不必要的信息写入数据库,因此数据库的大小会突飞猛进。 我什至不敢估计每周和每月图表的底部大小。
此外,电表本身并不能解决问题。 电表产生的值的图形是一个单调递增函数,每小时重置为 0。 我们需要一个用户可以理解的消耗图表,显示在此期间消耗了多少升。 标准历史图组件无法做到这一点,但迷你图卡外部组件可以帮助我们。
这是lovelace-UI 的卡代码:
- aggregate_func: max
entities:
- color: var(--primary-color)
entity: sensor.water_cold_hour_um
group_by: hour
hours_to_show: 48
name: "Hourly water consumption aggregated by utility meter"
points_per_hour: 1
show:
graph: bar
type: 'custom:mini-graph-card'
除了传感器名称、图形类型、颜色(我不喜欢标准橙色)等标准设置外,还需要注意 3 个设置:
- group_by:hour — 生成的图表中的条形与小时的开始对齐
- point_per_hour: 1 - 每小时一根柱
- 最重要的是,aggregate_func: max - 取每小时内的最大值。 正是这个参数将锯齿图变成了条形图
不要注意左侧的行列 - 这是没有数据时组件的标准行为。 但没有数据 - 我只是为了本文而在几个小时前打开了公用事业仪表数据收集(我将在下面描述我当前的方法)。
在这张图片中,我想表明有时数据显示甚至可以工作,并且条形实际上反映了正确的值。 但这还不是全部。 由于某种原因,所选的上午 11 点到 12 点期间的列显示为 19 升,但在同一传感器的同一时间段的齿状图表上,我们看到消耗量为 62 升。 要么是有bug,要么是手歪了。 但我还是不明白为什么右边的数据会中断——那里的消费是正常的,这从牙齿图上也可以看出。
总的来说,我无法实现这种方法的合理性——图表几乎总是显示出某种异端邪说。
日间传感器的类似代码。
- aggregate_func: max
entities:
- color: var(--primary-color)
entity: sensor.water_cold_day_um
group_by: interval
hours_to_show: 360
name: "Daily water consumption aggregated by utility meter"
points_per_hour: 0.0416666666
show:
graph: bar
type: 'custom:mini-graph-card'
请注意,group_by 参数设置为间隔,points_per_hour 参数决定一切。 该组件存在另一个问题 - point_per_hour 在一小时或更短的图表上运行良好,但在较大的时间间隔上却很糟糕。 因此,为了在一天内获得一列,我必须输入值 1/24=0.04166666。 我什至没有谈论每周和每月的图表。
方法2
当我还在了解家庭助理时,我看到了这个视频:
一位朋友收集了几种小米插座的消费数据。 他的任务稍微简单一些 - 只需显示今天、昨天和本月的消费值。 无需时间表。
我们先把关于手动积分瞬时功率值的讨论放在一边——我上面已经写过关于这种方法的“准确性”的内容。 目前尚不清楚他为何不使用累积消费值,这些消费值已经由同一家商店收集。 我认为,硬件内部的集成效果会更好。
从视频中,我们将采用手动计算一段时间内的消耗量的想法。 这家伙只计算了今天和昨天的值,但我们会更进一步并尝试绘制图表。 在我的案例中,所提出的方法的本质如下。
让我们创建一个变量 value_at_the_beginning_of_hour,我们将在其中记录当前的仪表读数
使用计时器,在一小时结束时(或下一小时开始时),我们计算当前读数与一小时开始时存储的读数之间的差异。 这个差异将是当前小时的消耗量 - 我们将把该值保存到传感器中,将来我们将根据该值构建一个图表。
您还需要通过写入当前计数器值来“重置” value_at_beginning_of_hour 变量。
所有这些都可以通过家庭助理本身来完成。
与之前的方法相比,您将需要编写更多的代码。 首先,让我们创建这些相同的“变量”。 我们没有开箱即用的“变量”实体,但我们可以使用 mqtt 代理的服务。 我们将使用保留 = true 标志向那里发送值 - 这会将值保存在代理内,并且可以随时将其从那里取出,即使家庭助理重新启动也是如此。 我同时制作了每小时和每日计数器。
- platform: mqtt
state_topic: "test/water/hour"
name: water_hour
unit_of_measurement: l
- platform: mqtt
state_topic: "test/water/hour_begin"
name: water_hour_begin
unit_of_measurement: l
- platform: mqtt
state_topic: "test/water/day"
name: water_day
unit_of_measurement: l
- platform: mqtt
state_topic: "test/water/day_begin"
name: water_day_begin
unit_of_measurement: l
所有的魔力都发生在自动化中,它分别每小时和每晚运行。
- id: water_new_hour
alias: water_new_hour
initial_state: true
trigger:
- platform: time_pattern
minutes: 0
action:
- service: mqtt.publish
data:
topic: "test/water/hour"
payload_template: >
{{ (states.sensor.water_meter_cold.state|int) - (states.sensor.water_hour_begin.state|int) }}
retain: true
- service: mqtt.publish
data:
topic: "test/water/hour_begin"
payload_template: >
{{ states.sensor.water_meter_cold.state }}
retain: true
- id: water_new_day
alias: water_new_day
initial_state: true
trigger:
- platform: time
at: "00:00:00"
action:
- service: mqtt.publish
data:
topic: "test/water/day"
payload_template: >
{{ (states.sensor.water_meter_cold.state|int) - (states.sensor.water_day_begin.state|int) }}
retain: true
- service: mqtt.publish
data:
topic: "test/water/day_begin"
payload_template: >
{{ states.sensor.water_meter_cold.state }}
retain: true
两个自动化执行 2 个操作:
- 计算间隔的值作为开始值和结束值之间的差值
- 更新下一个间隔的基值
这种情况下图的构造是通过通常的历史图来解决的:
- type: history-graph
title: 'Hourly water consumption using vars'
hours_to_show: 48
entities:
- sensor.water_hour
- type: history-graph
title: 'Daily water consumption using vars'
hours_to_show: 360
entities:
- sensor.water_day
它看起来像这样:
原则上,这已经是所需要的了。 这种方法的优点是每个时间间隔生成一次数据。 那些。 每小时图表每天只有 24 条记录。
不幸的是,这仍然不能解决基数不断增长的普遍问题。 如果我想要每月的消费图表,我将必须存储至少一年的数据。 而且由于家庭助手只为整个数据库提供一个存储期限设置,这意味着系统中的所有数据都必须存储一整年。 例如,我一年消耗 200 立方米的水,这意味着数据库中有 200000 个条目。 如果考虑到其他传感器,那么这个数字通常会变得不雅。
方法3
幸运的是,聪明人已经通过编写 InfluxDB 数据库解决了这个问题。 该数据库专门针对存储基于时间的数据进行了优化,非常适合存储不同传感器的值。 系统还提供了类似SQL的查询语言,允许您从数据库中提取值,然后以各种方式聚合它们。 最后,不同的数据可以存储不同的时间。 例如,温度或湿度等经常变化的读数只能存储几周,而每日耗水量读数则可以存储一整年。
除了InfluxDB之外,聪明人还发明了Grafana,一个基于InfluxDB数据绘制图表的系统。 Grafana 可以绘制不同类型的图表,对其进行详细定制,最重要的是,这些图表可以“插入”到 lovelace-UI 家庭助手上。
得到启发
因此,首先,让我们开始在 influxDB 中添加计数器值。 家庭助理配置的一部分(在这个例子中,我不仅可以使用冷水,还可以使用热水):
influxdb:
host: localhost
max_retries: 3
default_measurement: state
database: homeassistant
include:
entities:
- sensor.water_meter_hot
- sensor.water_meter_cold
让我们禁止将相同的数据保存到内部家庭助理数据库中,以免再次使其膨胀:
recorder:
purge_keep_days: 10
purge_interval: 1
exclude:
entities:
- sensor.water_meter_hot
- sensor.water_meter_cold
现在让我们转到 InfluxDB 控制台并配置我们的数据库。 特别是,您需要配置某些数据的存储时间。 这是由所谓的规定的。 保留策略 - 这类似于主数据库中的数据库,每个内部数据库都有自己的设置。 默认情况下,所有数据都存储在名为 autogen 的保留策略中;该数据将存储一周。 我希望每小时数据保留一个月,每周数据保留一年,每月数据永远不会被删除。 让我们创建适当的保留策略
CREATE RETENTION POLICY "month" ON "homeassistant" DURATION 30d REPLICATION 1
CREATE RETENTION POLICY "year" ON "homeassistant" DURATION 52w REPLICATION 1
CREATE RETENTION POLICY "infinite" ON "homeassistant" DURATION INF REPLICATION 1
事实上,现在的主要技巧是使用连续查询进行数据聚合。 这是一种按指定时间间隔自动运行查询、聚合该查询的数据并将结果添加到新值中的机制。 让我们看一个例子(为了便于阅读,我写在一列中,但实际上我必须在一行中输入此命令)
CREATE CONTINUOUS QUERY cq_water_hourly ON homeassistant
BEGIN
SELECT max(value) AS value
INTO homeassistant.month.water_meter_hour
FROM homeassistant.autogen.l
GROUP BY time(1h), entity_id fill(previous)
END
这个命令:
- 在 homeassistant 数据库中创建名为 cq_water_cold_hourly 的连续查询
- 该请求将每小时执行一次 (time(1h))
- 该请求将从测量'homeassistant.autogen.l(升)中抓取所有数据,包括冷水和热水读数
- 聚合数据将按entity_id分组,这将为我们提供冷水和热水的单独值
- 由于升计数器是每小时内单调递增的序列,因此需要取最大值,因此将通过函数 max(value) 进行聚合
- 新值将写入 homeassistant.month.water_meter_hour,其中 Month 是保留期为 XNUMX 个月的保留策略的名称。 此外,冷水和热水的数据将分散到单独的记录中,并在value字段中具有相应的entity_id和value
在夜间或无人在家时,没有水消耗,因此 homeassistant.autogen.l 中没有新条目。 为了避免常规查询中缺失值,可以使用 fill(previous)。 这将强制 InfluxDB 使用最后一小时的值。
不幸的是,连续查询有一个特点:填充(前一个)技巧不起作用,并且根本不会创建记录。 而且,这是一个无法克服的问题
让我们检查一下发生了什么(当然,你需要等待几个小时):
> select * from homeassistant.month.water_meter_hour group by entity_id
...
name: water_meter_hour
tags: entity_id=water_meter_cold
time value
---- -----
...
2020-03-08T01:00:00Z 370511
2020-03-08T02:00:00Z 370513
2020-03-08T05:00:00Z 370527
2020-03-08T06:00:00Z 370605
2020-03-08T07:00:00Z 370635
2020-03-08T08:00:00Z 370699
2020-03-08T09:00:00Z 370761
2020-03-08T10:00:00Z 370767
2020-03-08T11:00:00Z 370810
2020-03-08T12:00:00Z 370818
2020-03-08T13:00:00Z 370827
2020-03-08T14:00:00Z 370849
2020-03-08T15:00:00Z 370921
请注意,数据库中的值以 UTC 存储,因此此列表相差 3 小时 - InfluxDB 输出中的上午 7 点值对应于上图中上午 10 点的值。 另请注意,凌晨 2 点到 5 点之间根本没有记录 - 这与连续查询的特征相同。
正如您所看到的,聚合值也是单调递增的序列,只是条目出现的频率较低 - 每小时一次。 但这不是问题 - 我们可以编写另一个查询来检索图表的正确数据。
SELECT difference(max(value))
FROM homeassistant.month.water_meter_hour
WHERE entity_id='water_meter_cold' and time >= now() -24h
GROUP BY time(1h), entity_id
fill(previous)
我会解密:
- 我们将从 homeassistant.month.water_meter_hour 数据库中提取实体_id='water_meter_cold' 最后一天(时间 >= now() -24h)的数据。
- 正如我已经提到的,homeassistant.month.water_meter_hour 序列中可能缺少一些条目。 我们将通过使用 GROUP BY time(1h) 运行查询来重新生成此数据。 这次 fill(previous) 将按预期工作,生成丢失的数据(该函数将采用以前的值)
- 这个请求中最重要的是差异函数,它将计算小时标记之间的差异。 它本身不能工作,需要聚合函数。 让它成为之前使用的 max() 。
执行结果是这样的
name: water_meter_hour
tags: entity_id=water_meter_cold
time difference
---- ----------
...
2020-03-08T02:00:00Z 2
2020-03-08T03:00:00Z 0
2020-03-08T04:00:00Z 0
2020-03-08T05:00:00Z 14
2020-03-08T06:00:00Z 78
2020-03-08T07:00:00Z 30
2020-03-08T08:00:00Z 64
2020-03-08T09:00:00Z 62
2020-03-08T10:00:00Z 6
2020-03-08T11:00:00Z 43
2020-03-08T12:00:00Z 8
2020-03-08T13:00:00Z 9
2020-03-08T14:00:00Z 22
2020-03-08T15:00:00Z 72
凌晨 2 点至 5 点(UTC)没有任何消费。 尽管如此,由于 fill(previous),查询将返回相同的消耗值,并且差异函数将从自身中减去该值,并且输出将为 0,这正是所需要的。
剩下的就是构建一个图表。 为此,请打开 Grafana,打开一些现有(或创建新的)仪表板,然后创建一个新面板。 图表设置将是这样的。
我将在同一张图表上显示冷水和热水数据。 该请求与我上面描述的完全相同。
显示参数设置如下。 对我来说,这将是一个带有线条的图表,呈阶梯状(楼梯)。 我将在下面解释 Stack 参数。 下面还有几个显示选项,但它们并不是那么有趣。
要将生成的图表添加到家庭助理,您需要:
- 退出图表编辑模式。 由于某种原因,仅从仪表板页面提供正确的图表共享设置
- 单击图表名称旁边的三角形,然后从菜单中选择共享
- 在打开的窗口中,转到嵌入选项卡
- 取消选中当前时间范围 - 我们将通过 URL 设置时间范围
- 选择所需的主题。 就我而言,它很轻
- 将生成的 URL 复制到 lovelace-UI 设置卡
- type: iframe
id: graf_water_hourly
url: "http://192.168.10.200:3000/d-solo/rZARemQWk/water?orgId=1&panelId=2&from=now-2d&to=now&theme=light"
请注意,时间范围(最近 2 天)是在此处设置的,而不是在仪表板设置中设置的。
该图看起来像这样。 最近两天没有用过热水,所以只画了冷水图。
我还没有自己决定我更喜欢哪种图表,是线步图还是真实条形图。 因此,我将简单地给出一个每日消耗图表的例子,只是这次是在柱形图上。 查询的构造与上述类似。 显示选项有:
该图如下所示:
关于 Stack 参数。 在此图中,冷水柱绘制在热水柱的顶部。 总高度对应于该时段的冷水和热水的总消耗量。
显示的所有图表都是动态的。 您可以将鼠标悬停在感兴趣的点上,查看特定点的详细信息和价值。
不幸的是,有一些美中不足。 在条形图上(与带有阶梯线的图表不同),条形图的中间不是在一天的中间,而是在 00:00。 那些。 该栏的左半部分是在前一天的位置绘制的。 因此,周六和周日的图表稍微绘制在蓝色区域的左侧。 直到我弄清楚如何打败它。
另一个问题是无法按月正常工作。 事实上,小时/天/周的长度是固定的,但月份的长度每次都不同。 InfluxDB只能等间隔工作。 到目前为止我的脑子已经足够设定30天的固定间隔了。 是的,图表在一年中会稍微浮动,并且条形图不会与月份完全对应。 但由于我对这个东西只是作为一个显示仪表感兴趣,所以我同意它。
我看到至少有两种解决方案:
- 放弃月度图表并限制自己只关注周度图表。 今年 52 条周线看起来相当不错
- 将每月消费本身视为方法 2,并且仅使用 grafana 来获得漂亮的图表。 这将是一个非常准确的解决方案。 您甚至可以叠加过去一年的图表进行比较 - grafana 也可以做到这一点。
结论
我不知道为什么,但我对这类图表很着迷。 它们表明生活如火如荼,一切都在变化。 昨天很多,今天很少,明天又会是别的。 剩下的就是与家庭成员就消费话题进行合作。 但即使按照目前的胃口,只是付款单上一个大而难以理解的数字已经变成了相当可以理解的消费图景。
尽管我有近 20 年的程序员职业生涯,但我几乎没有接触过数据库。 因此,安装外部数据库似乎是一件深奥难懂的事情。 改变了一切
在标题中我提到了电力消耗。 不幸的是,目前我无法提供任何图表。 一台 SDM120 仪表对我来说已经死了,另一台在通过 Modbus 访问时出现故障。 然而,这不会以任何方式影响本文的主题 - 图表将以与水相同的方式构建。
在这篇文章中,我介绍了我自己尝试过的方法。 当然还有一些我不知道的其他组织数据收集和可视化的方法。 在评论中告诉我,我会很感兴趣。 我很乐意接受建设性的批评和新想法。 我希望所提供的材料也能对某人有所帮助。
来源: habr.com