gw_data 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {gw_data-0.3.0 → gw_data-0.3.2}/PKG-INFO +2 -39
  2. {gw_data-0.3.0 → gw_data-0.3.2}/README.md +1 -38
  3. {gw_data-0.3.0 → gw_data-0.3.2}/alembic/env.py +2 -1
  4. gw_data-0.3.2/alembic/versions/a7f151e8163f_visualizer_functions_and_views.py +74 -0
  5. {gw_data-0.3.0 → gw_data-0.3.2}/pyproject.toml +1 -1
  6. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/__init__.py +2 -0
  7. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/reading.py +1 -1
  8. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/user.py +1 -1
  9. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/user_installation_role.py +0 -1
  10. gw_data-0.3.2/src/gw_data/db/scripts/misc/adjust_report_message_timestamps.sql +59 -0
  11. gw_data-0.3.2/src/gw_data/db/scripts/misc/create_policy_view_readings_1hr.sql +4 -0
  12. gw_data-0.3.2/src/gw_data/db/scripts/misc/create_proc_refresh_cached_hourly_data.sql +20 -0
  13. gw_data-0.3.2/src/gw_data/db/scripts/misc/create_table_cached_hourly_data.sql +41 -0
  14. gw_data-0.3.2/src/gw_data/db/scripts/misc/create_udf_calc_energy_1hr.sql +56 -0
  15. gw_data-0.3.2/src/gw_data/db/scripts/misc/create_udf_calc_hourly_data.sql +162 -0
  16. gw_data-0.3.2/src/gw_data/db/scripts/misc/create_udf_retrieve_readings_1s.sql +40 -0
  17. gw_data-0.3.2/src/gw_data/db/scripts/misc/create_view_readings_1hr.sql +15 -0
  18. gw_data-0.3.2/src/gw_data/db/scripts/misc/refresh_all_cached_hourly_data.py +46 -0
  19. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/scripts/seed_data/homes.csv +1 -0
  20. gw_data-0.3.2/useful-scripts.sql +70 -0
  21. {gw_data-0.3.0 → gw_data-0.3.2}/uv.lock +1 -1
  22. gw_data-0.3.0/postgres_views.md +0 -44
  23. {gw_data-0.3.0 → gw_data-0.3.2}/.github/workflows/release.yml +0 -0
  24. {gw_data-0.3.0 → gw_data-0.3.2}/.gitignore +0 -0
  25. {gw_data-0.3.0 → gw_data-0.3.2}/alembic/README +0 -0
  26. {gw_data-0.3.0 → gw_data-0.3.2}/alembic/script.py.mako +0 -0
  27. {gw_data-0.3.0 → gw_data-0.3.2}/alembic/versions/1220f2f941dd_initial_schema.py +0 -0
  28. {gw_data-0.3.0 → gw_data-0.3.2}/alembic.ini +0 -0
  29. {gw_data-0.3.0 → gw_data-0.3.2}/sema_seed_request.yaml +0 -0
  30. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/__init__.py +0 -0
  31. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/config.py +0 -0
  32. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/__init__.py +0 -0
  33. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/_base.py +0 -0
  34. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/connectivity_edge.py +0 -0
  35. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/customer.py +0 -0
  36. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/g_node.py +0 -0
  37. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/installation.py +0 -0
  38. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/installer.py +0 -0
  39. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/message.py +0 -0
  40. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/position_point.py +0 -0
  41. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/models/reading_channel.py +0 -0
  42. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/scripts/0_db_create.psql +0 -0
  43. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/scripts/1_db_user_setup.psql +0 -0
  44. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/scripts/2_db_schema_setup.sql +0 -0
  45. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/scripts/3_db_alembic_upgrade.sh +0 -0
  46. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/scripts/4_db_seed.py +0 -0
  47. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/scripts/_XX_drop_all.sql +0 -0
  48. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/db/session.py +0 -0
  49. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/__init__.py +0 -0
  50. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/base.py +0 -0
  51. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/codec.py +0 -0
  52. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/definitions/enums/base.g.node.class/000.yaml +0 -0
  53. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/definitions/enums/g.node.status/000.yaml +0 -0
  54. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/definitions/formats/left.right.dot.yaml +0 -0
  55. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/definitions/formats/uuid4.str.yaml +0 -0
  56. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/definitions/registry.yaml +0 -0
  57. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/definitions/types/g.node.gt/004.yaml +0 -0
  58. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/definitions/types/position.point.gt/000.yaml +0 -0
  59. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/enums/__init__.py +0 -0
  60. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/enums/base_g_node_class.py +0 -0
  61. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/enums/g_node_status.py +0 -0
  62. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/enums/gw_str_enum.py +0 -0
  63. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/enums/old_versions/__init__.py +0 -0
  64. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/indexes/dependency_closure.yaml +0 -0
  65. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/indexes/local_names.yaml +0 -0
  66. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/indexes/lookup.yaml +0 -0
  67. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/indexes/reverse_dependencies.yaml +0 -0
  68. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/indexes/seed_expanded.yaml +0 -0
  69. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/indexes/versions.yaml +0 -0
  70. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/property_format.py +0 -0
  71. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/tests/test_property_format.py +0 -0
  72. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/types/__init__.py +0 -0
  73. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/types/g_node_gt.py +0 -0
  74. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/types/old_versions/__init__.py +0 -0
  75. {gw_data-0.3.0 → gw_data-0.3.2}/src/gw_data/sema/types/position_point_gt.py +0 -0
  76. {gw_data-0.3.0 → gw_data-0.3.2}/template.env +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gw_data
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Gridworks Data Infrastructure
5
5
  Author-email: Jessica Millar <jessica.lynn.millar@gmail.com>
6
6
  Requires-Python: <3.13.0,>=3.12
@@ -149,41 +149,4 @@ We have column-store compression enabled on the readings table for data older th
149
149
 
150
150
  ### Useful Queries
151
151
 
152
- The following queries are useful for analyzing TimescaleDB performance:
153
-
154
- ```
155
- -- Display the size in MB of our two main tables
156
- SELECT pg_size_pretty(hypertable_size('readings')) as "Readings Table Size", pg_size_pretty(hypertable_size('messages')) as "Messages Table Size";
157
- ```
158
-
159
- ```
160
- -- Display the size of all database tables in order.
161
- -- This will show each TimescaleDB chunk as a separate table.
162
- SELECT
163
- table_schema || '.' || table_name AS table_full_name,
164
- pg_size_pretty(pg_total_relation_size('"' || table_schema || '"."' || table_name || '"')) AS size
165
- FROM information_schema.tables
166
- ORDER BY
167
- pg_total_relation_size('"' || table_schema || '"."' || table_name || '"') DESC;
168
-
169
- ```
170
-
171
- ```
172
- -- Display compression stats for each chunk in the readings table
173
- SELECT
174
- chunk_name,
175
- pg_size_pretty(before_compression_total_bytes) AS size_before,
176
- pg_size_pretty(after_compression_total_bytes) AS size_after,
177
- 100 - (after_compression_total_bytes::float / before_compression_total_bytes * 100) AS compression_ratio_pct
178
- FROM chunk_compression_stats('readings');
179
- ```
180
-
181
- ```
182
- -- Get the ID of the policy_compression job so you can manually run `CALL run_job` with it (e.g. after a bulk import).
183
- SELECT job_id, proc_name, hypertable_name, config
184
- FROM timescaledb_information.jobs;
185
- ```
186
- ```
187
- -- Get info (table, time range, etc.) about the TimescaleDB chunks.
188
- SELECT * FROM timescaledb_information.chunks
189
- ```
152
+ There are some useful queries listed in `useful-queries.sql` in the project root.
@@ -134,41 +134,4 @@ We have column-store compression enabled on the readings table for data older th
134
134
 
135
135
  ### Useful Queries
136
136
 
137
- The following queries are useful for analyzing TimescaleDB performance:
138
-
139
- ```
140
- -- Display the size in MB of our two main tables
141
- SELECT pg_size_pretty(hypertable_size('readings')) as "Readings Table Size", pg_size_pretty(hypertable_size('messages')) as "Messages Table Size";
142
- ```
143
-
144
- ```
145
- -- Display the size of all database tables in order.
146
- -- This will show each TimescaleDB chunk as a separate table.
147
- SELECT
148
- table_schema || '.' || table_name AS table_full_name,
149
- pg_size_pretty(pg_total_relation_size('"' || table_schema || '"."' || table_name || '"')) AS size
150
- FROM information_schema.tables
151
- ORDER BY
152
- pg_total_relation_size('"' || table_schema || '"."' || table_name || '"') DESC;
153
-
154
- ```
155
-
156
- ```
157
- -- Display compression stats for each chunk in the readings table
158
- SELECT
159
- chunk_name,
160
- pg_size_pretty(before_compression_total_bytes) AS size_before,
161
- pg_size_pretty(after_compression_total_bytes) AS size_after,
162
- 100 - (after_compression_total_bytes::float / before_compression_total_bytes * 100) AS compression_ratio_pct
163
- FROM chunk_compression_stats('readings');
164
- ```
165
-
166
- ```
167
- -- Get the ID of the policy_compression job so you can manually run `CALL run_job` with it (e.g. after a bulk import).
168
- SELECT job_id, proc_name, hypertable_name, config
169
- FROM timescaledb_information.jobs;
170
- ```
171
- ```
172
- -- Get info (table, time range, etc.) about the TimescaleDB chunks.
173
- SELECT * FROM timescaledb_information.chunks
174
- ```
137
+ There are some useful queries listed in `useful-queries.sql` in the project root.
@@ -10,8 +10,9 @@ from gw_data.config import Settings
10
10
  GW_SCHEMA = "gridworks"
11
11
  def include_name(name, type_, parent_names):
12
12
  if type_ == "schema":
13
- # Only track migrations in your chosen schema
14
13
  return name == GW_SCHEMA
14
+ elif type_ == "table":
15
+ return name not in ['cached_hourly_data']
15
16
  return True
16
17
 
17
18
  # -----------------------------------------------------------------------------
@@ -0,0 +1,74 @@
1
+ """visualizer functions and views
2
+
3
+ Revision ID: a7f151e8163f
4
+ Revises: 1220f2f941dd
5
+ Create Date: 2026-06-18 10:34:33.737411
6
+
7
+ """
8
+ import os
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = 'a7f151e8163f'
17
+ down_revision: Union[str, Sequence[str], None] = '1220f2f941dd'
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Upgrade schema."""
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ # ### end Alembic commands ###
26
+
27
+ op.execute("GRANT UPDATE (last_login, updated_at) ON gridworks.users TO gw_visualizer;")
28
+
29
+ op.execute("CREATE FUNCTION gridworks.c2f(temp_c FLOAT) RETURNS FLOAT IMMUTABLE LANGUAGE 'sql' RETURN temp_c * 9.0 / 5.0 + 32;")
30
+ op.execute("CREATE FUNCTION gridworks.f2c(temp_f FLOAT) RETURNS FLOAT IMMUTABLE LANGUAGE 'sql' RETURN (temp_f - 32) * 5.0 / 9.0;")
31
+
32
+ misc_scripts_folder = os.path.join(
33
+ os.path.dirname(__file__),
34
+ '../../src/gw_data/db/scripts/misc/',
35
+ )
36
+
37
+ for script in [
38
+ 'create_view_readings_1hr.sql',
39
+ 'create_policy_view_readings_1hr.sql',
40
+ 'create_udf_retrieve_readings_1s.sql',
41
+ 'create_udf_calc_energy_1hr.sql',
42
+ 'create_udf_calc_hourly_data.sql',
43
+ 'create_table_cached_hourly_data.sql',
44
+ 'create_proc_refresh_cached_hourly_data.sql'
45
+ ]:
46
+ with open(os.path.join(misc_scripts_folder, script), 'r') as f:
47
+ sql = f.read().strip()
48
+ op.execute(sql)
49
+
50
+
51
+ op.execute("SELECT add_job('gridworks.refresh_cached_hourly_data', INTERVAL '30 minutes');")
52
+
53
+ print("""
54
+ IMPORTANT: to populate existing data into the `gridworks.readings_1hr` continuous aggregate, you must manually run `refresh_continuous_aggregate`.
55
+ IMPORTANT: to populate existing data into the `gridworks.cached_hourly_data` table, you will need to run `refresh_all_cached_hourly_data.py`.
56
+ """)
57
+
58
+
59
+
60
+ def downgrade() -> None:
61
+ """Downgrade schema."""
62
+ # ### commands auto generated by Alembic - please adjust! ###
63
+
64
+ # ### end Alembic commands ###
65
+
66
+ op.execute("SELECT delete_job((SELECT job_id FROM timescaledb_information.jobs WHERE proc_schema = 'gridworks' AND proc_name = 'refresh_cached_hourly_data'));")
67
+ op.execute('DROP PROCEDURE gridworks.refresh_cached_hourly_data;')
68
+ op.execute('DROP TABLE gridworks.cached_hourly_data;')
69
+ op.execute('DROP FUNCTION gridworks.calc_hourly_data;')
70
+ op.execute('DROP MATERIALIZED VIEW gridworks.readings_1hr;')
71
+ op.execute('DROP FUNCTION gridworks.retrieve_readings_1s(timestamp with time zone, timestamp with time zone, text[]);')
72
+ op.execute('DROP FUNCTION gridworks.calc_energy_1hr(timestamp with time zone, timestamp with time zone);')
73
+ op.execute('DROP FUNCTION gridworks.c2f(float)')
74
+ op.execute('DROP FUNCTION gridworks.f2c(float)')
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gw_data"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Gridworks Data Infrastructure"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,6 +8,7 @@ from gw_data.db.models.customer import CustomerSql
8
8
  from gw_data.db.models.installer import InstallerSql
9
9
  from gw_data.db.models.user import UserSql
10
10
  from gw_data.db.models.installation import InstallationSql
11
+ from gw_data.db.models.user_installation_role import UserInstallationRoleSql
11
12
 
12
13
  from gw_data.db.models.reading_channel import ReadingChannelSql
13
14
  from gw_data.db.models.message import MessageSql
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "InstallerSql",
32
33
  "UserSql",
33
34
  "InstallationSql",
35
+ "UserInstallationRoleSql",
34
36
  "ReadingChannelSql",
35
37
  "MessageSql",
36
38
  "ReadingSql"
@@ -39,7 +39,7 @@ class ReadingSql(Base):
39
39
  index=True
40
40
  )
41
41
  timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
42
- value: Mapped[BigInteger] = mapped_column(BigInteger, nullable=False)
42
+ value: Mapped[int] = mapped_column(BigInteger, nullable=False)
43
43
 
44
44
  __table_args__ = (
45
45
  # TimescaleDB automatically creates this one; we need to include it so Alembic doesn't get confused
@@ -29,7 +29,7 @@ class UserSql(Base):
29
29
  created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
30
30
  updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
31
31
 
32
- installation_roles: Mapped[list[UserInstallationRoleSql]] = relationship(uselist=True, lazy="joined")
32
+ installation_roles: Mapped[list[UserInstallationRoleSql]] = relationship(uselist=True)
33
33
 
34
34
  def __repr__(self) -> str:
35
35
  return f"User(id={self.id!r}, username={self.username!r}, is_active={self.is_active!r})"
@@ -23,7 +23,6 @@ class UserInstallationRoleSql(Base):
23
23
  installations: Mapped[list[InstallationSql]] = relationship(
24
24
  "InstallationSql",
25
25
  uselist=True,
26
- lazy="joined",
27
26
  primaryjoin="or_(InstallationSql.id==UserInstallationRoleSql.installation_id, UserInstallationRoleSql.installation_id == None)"
28
27
  )
29
28
 
@@ -0,0 +1,59 @@
1
+ -- report.event message parsing had a bug that caused the timestamps to get set in local time rather than UTC.
2
+ -- This script fixes them.
3
+ -- (Unfortunately we can't just UPDATE a timestamp column with TimescaleDB)
4
+ -- This never needed to be run in prod, since the server local time was UTC.
5
+
6
+ -- SET temp_buffers = '2000MB';
7
+
8
+ BEGIN;
9
+
10
+ CREATE TEMP TABLE temp_report_messages AS
11
+ SELECT
12
+ id,
13
+ "timestamp" + INTERVAL '6 hours' AS "timestamp",
14
+ from_alias,
15
+ created_at + INTERVAL '6 hours' AS created_at,
16
+ persisted_at,
17
+ message_type_name,
18
+ payload
19
+ FROM gridworks.messages
20
+ -- where id='85c153a2-ebf6-4a24-83ba-5dba12967d87';
21
+ WHERE message_type_name='report.event';
22
+
23
+ DELETE FROM gridworks.messages
24
+ -- where id='85c153a2-ebf6-4a24-83ba-5dba12967d87';
25
+ WHERE message_type_name='report.event';
26
+
27
+ INSERT INTO gridworks.messages SELECT * FROM temp_report_messages;
28
+
29
+ WITH
30
+ q1 AS (
31
+ SELECT COUNT(*) as cnt, ROW_NUMBER() OVER () AS rn FROM gridworks.messages
32
+ ),
33
+ q2 AS (
34
+ SELECT *, ROW_NUMBER() OVER () AS rn
35
+ FROM gridworks.messages
36
+ where id='85c153a2-ebf6-4a24-83ba-5dba12967d87'
37
+ )
38
+ SELECT
39
+ q1.cnt,
40
+ q2.*
41
+ FROM q1
42
+ FULL OUTER JOIN q2 ON q1.rn = q2.rn;
43
+
44
+ -- Fix the OAT and WS readings
45
+ select id from gridworks.reading_channels where name = 'forecast-ws';
46
+ update gridworks.readings
47
+ set value=value * 10
48
+ where channel_id in (
49
+ '792a01bc-1b95-4434-97e5-30d64c000024',
50
+ '3310111e-33ab-4c07-bab1-7d4c7d464201',
51
+ '60bb6806-e5a5-4a06-a60e-c26b0bf77e59',
52
+ 'a349f893-61a7-4c65-8e33-998517d06fb0',
53
+ 'f0ae219e-73d1-4c61-9c02-a13e940b8737',
54
+ '97a42794-bb6b-4a4d-be6f-1c5c9f955af7'
55
+ )
56
+
57
+
58
+ COMMIT
59
+ -- ROLLBACK;
@@ -0,0 +1,4 @@
1
+ SELECT add_continuous_aggregate_policy('gridworks.readings_1hr',
2
+ start_offset => INTERVAL '2 days',
3
+ end_offset => INTERVAL '1 hour',
4
+ schedule_interval => INTERVAL '30 minutes');
@@ -0,0 +1,20 @@
1
+ CREATE OR REPLACE PROCEDURE gridworks.refresh_cached_hourly_data(job_id int, config jsonb)
2
+ LANGUAGE plpgsql
3
+ AS $$
4
+ DECLARE
5
+ t_start timestamp with time zone := now() - INTERVAL '2 days';
6
+ t_start_valid timestamp with time zone := t_start + INTERVAL '1 hour';
7
+ t_end timestamp with time zone := now();
8
+ BEGIN
9
+ DELETE FROM gridworks.cached_hourly_data
10
+ WHERE time_bucket >= t_start_valid AND time_bucket <= t_end;
11
+
12
+ INSERT INTO gridworks.cached_hourly_data
13
+ WITH hourly_data AS MATERIALIZED (
14
+ SELECT * FROM gridworks.calc_hourly_data(t_start => t_start, t_end => t_end)
15
+ )
16
+ SELECT * FROM hourly_data
17
+ WHERE time_bucket >= t_start_valid;
18
+ END;
19
+ $$;
20
+
@@ -0,0 +1,41 @@
1
+ CREATE TABLE gridworks.cached_hourly_data (
2
+ terminal_asset_alias text,
3
+ time_bucket timestamp with time zone,
4
+ hp_kwh_el float,
5
+ hp_kwh_th float,
6
+ dist_kwh float,
7
+ store_change_kwh float,
8
+ hp_avg_lwt float,
9
+ hp_avg_ewt float,
10
+ dist_avg_swt float,
11
+ dist_avg_rwt float,
12
+ buffer_depth1_start float,
13
+ buffer_depth2_start float,
14
+ buffer_depth3_start float,
15
+ buffer_depth4_start float,
16
+ tank1_depth1_start float,
17
+ tank1_depth2_start float,
18
+ tank1_depth3_start float,
19
+ tank1_depth4_start float,
20
+ tank2_depth1_start float,
21
+ tank2_depth2_start float,
22
+ tank2_depth3_start float,
23
+ tank2_depth4_start float,
24
+ tank3_depth1_start float,
25
+ tank3_depth2_start float,
26
+ tank3_depth3_start float,
27
+ tank3_depth4_start float,
28
+ relay_3_pulled_fraction float,
29
+ relay_5_pulled_fraction float,
30
+ relay_6_pulled_fraction float,
31
+ relay_9_pulled_fraction float,
32
+ zone1_heatcall_fraction float,
33
+ zone2_heatcall_fraction float,
34
+ zone3_heatcall_fraction float,
35
+ zone4_heatcall_fraction float,
36
+ ws_mph float,
37
+ oat_f float,
38
+ total_usd_per_mwh float,
39
+ buffer_available_kwh float,
40
+ lmp_usd_per_mwh float
41
+ )
@@ -0,0 +1,56 @@
1
+ CREATE FUNCTION gridworks.calc_energy_1hr(
2
+ t_start timestamp with time zone,
3
+ t_end timestamp with time zone)
4
+ RETURNS TABLE(
5
+ terminal_asset_alias text,
6
+ time_bucket timestamp with time zone,
7
+ hp_kwh float,
8
+ dist_kwh float,
9
+ store_change_kwh float
10
+ )
11
+ STABLE
12
+ LANGUAGE 'sql'
13
+
14
+ AS $BODY$
15
+
16
+ SELECT
17
+ terminal_asset_alias,
18
+ time_bucket(INTERVAL '1 hour', time_bucket_1s) AS time_bucket_1h,
19
+ avg(hp_kw) AS hp_kwh,
20
+ avg(dist_kw) AS dist_kwh,
21
+ avg(store_change_kw) AS store_change_kwh
22
+ FROM (
23
+ SELECT
24
+ *,
25
+ 500 * primary_flow_gpm * (hp_lwt_c - hp_ewt_c) * 9 / 5 / 3410 AS hp_kw,
26
+ 500 * dist_flow_gpm * (dist_swt_c - dist_rwt_c) * 9 / 5 / 3410 AS dist_kw,
27
+ 500 * (
28
+ (1 - store_charge_discharge_0_1) * store_flow_gpm +
29
+ store_charge_discharge_0_1 * primary_flow_gpm
30
+ ) * (store_charge_discharge_0_1 * 2 - 1) * (store_hot_pipe_c - store_cold_pipe_c) * 9 / 5 / 3410 AS store_change_kw
31
+ FROM (
32
+ SELECT
33
+ terminal_asset_alias,
34
+ time_bucket AS time_bucket_1s,
35
+ (AVG (value) FILTER (WHERE channel_name='primary-flow' AND unit='GpmTimes100') / 100) AS primary_flow_gpm,
36
+ (AVG (value) FILTER (WHERE channel_name='hp-lwt' AND unit='WaterTempCTimes1000') / 1000) AS hp_lwt_c,
37
+ (AVG (value) FILTER (WHERE channel_name='hp-ewt' AND unit='WaterTempCTimes1000') / 1000) AS hp_ewt_c,
38
+ (AVG (value) FILTER (WHERE channel_name='dist-flow' AND unit='GpmTimes100') / 100) AS dist_flow_gpm,
39
+ (AVG (value) FILTER (WHERE channel_name='dist-swt' AND unit='WaterTempCTimes1000') / 1000) AS dist_swt_c,
40
+ (AVG (value) FILTER (WHERE channel_name='dist-rwt' AND unit='WaterTempCTimes1000') / 1000) AS dist_rwt_c,
41
+ (AVG (value) FILTER (WHERE channel_name='store-flow' AND unit='GpmTimes100') / 100) AS store_flow_gpm,
42
+ (AVG (value) FILTER (WHERE channel_name='store-hot-pipe' AND unit='WaterTempCTimes1000') / 1000) AS store_hot_pipe_c,
43
+ (AVG (value) FILTER (WHERE channel_name='store-cold-pipe' AND unit='WaterTempCTimes1000') / 1000) AS store_cold_pipe_c,
44
+ (AVG (value) FILTER (WHERE channel_name='charge-discharge-relay3')) AS store_charge_discharge_0_1
45
+ FROM gridworks.retrieve_readings_1s(
46
+ t_start => t_start,
47
+ t_end => t_end,
48
+ channels => ARRAY['hp-ewt','hp-lwt', 'primary-flow', 'dist-swt', 'dist-rwt', 'dist-flow', 'charge-discharge-relay3', 'store-hot-pipe', 'store-cold-pipe', 'store-flow']
49
+ )
50
+ GROUP BY terminal_asset_alias,time_bucket
51
+ )
52
+ )
53
+
54
+ GROUP BY terminal_asset_alias,time_bucket_1h
55
+ ORDER BY terminal_asset_alias,time_bucket_1h
56
+ $BODY$;
@@ -0,0 +1,162 @@
1
+ CREATE FUNCTION gridworks.calc_hourly_data(
2
+ t_start timestamp with time zone,
3
+ t_end timestamp with time zone)
4
+ RETURNS TABLE(
5
+ -- NOTE that this table must match the cached_hourly_data table!
6
+ terminal_asset_alias text,
7
+ time_bucket timestamp with time zone,
8
+ hp_kwh_el float,
9
+ hp_kwh_th float,
10
+ dist_kwh float,
11
+ store_change_kwh float,
12
+ hp_avg_lwt float,
13
+ hp_avg_ewt float,
14
+ dist_avg_swt float,
15
+ dist_avg_rwt float,
16
+ buffer_depth1_start float,
17
+ buffer_depth2_start float,
18
+ buffer_depth3_start float,
19
+ buffer_depth4_start float,
20
+ tank1_depth1_start float,
21
+ tank1_depth2_start float,
22
+ tank1_depth3_start float,
23
+ tank1_depth4_start float,
24
+ tank2_depth1_start float,
25
+ tank2_depth2_start float,
26
+ tank2_depth3_start float,
27
+ tank2_depth4_start float,
28
+ tank3_depth1_start float,
29
+ tank3_depth2_start float,
30
+ tank3_depth3_start float,
31
+ tank3_depth4_start float,
32
+ relay_3_pulled_fraction float,
33
+ relay_5_pulled_fraction float,
34
+ relay_6_pulled_fraction float,
35
+ relay_9_pulled_fraction float,
36
+ zone1_heatcall_fraction float,
37
+ zone2_heatcall_fraction float,
38
+ zone3_heatcall_fraction float,
39
+ zone4_heatcall_fraction float,
40
+ ws_mph float,
41
+ oat_f float,
42
+ total_usd_per_mwh float,
43
+ buffer_available_kwh float,
44
+ lmp_usd_per_mwh float
45
+ )
46
+ STABLE
47
+ LANGUAGE 'sql'
48
+
49
+
50
+ AS $BODY$
51
+ SELECT terminal_asset_alias, time_bucket,
52
+ (hp_idu_w + hp_odu_w) / 1000 AS hp_kwh_el,
53
+ hp_kwh AS hp_kwh_th,
54
+ dist_kwh,
55
+ store_change_kwh,
56
+ hp_avg_lwt,hp_avg_ewt,
57
+ dist_avg_swt, dist_avg_rwt,
58
+ buffer_depth1_start, buffer_depth2_start, buffer_depth3_start, buffer_depth4_start,
59
+ tank1_depth1_start, tank1_depth2_start, tank1_depth3_start, tank1_depth4_start,
60
+ tank2_depth1_start, tank2_depth2_start, tank2_depth3_start, tank2_depth4_start,
61
+ tank3_depth1_start, tank3_depth2_start, tank3_depth3_start, tank3_depth4_start,
62
+ relay_3_pulled_fraction, relay_5_pulled_fraction, relay_6_pulled_fraction, relay_9_pulled_fraction,
63
+ zone1_heatcall_fraction, zone2_heatcall_fraction, zone3_heatcall_fraction, zone4_heatcall_fraction,
64
+ ws_mph, oat_f,
65
+ total_usd_per_mwh,
66
+ buffer_available_kwh,
67
+ lmp_usd_per_mwh
68
+ FROM
69
+ (
70
+ SELECT * FROM (
71
+ SELECT terminal_asset_alias, time_bucket, hp_kwh, dist_kwh, store_change_kwh
72
+ FROM gridworks.calc_energy_1hr(t_start => t_start, t_end => t_end)
73
+ ) AS kwh_values
74
+ JOIN (
75
+ SELECT
76
+ terminal_asset_alias AS avg_value_ta,
77
+ time_bucket AS avg_value_time_bucket,
78
+ AVG (avg_value) FILTER (WHERE channel_name='hp-idu-pwr' AND unit='PowerW') AS hp_idu_w,
79
+ AVG (avg_value) FILTER (WHERE channel_name='hp-odu-pwr' AND unit='PowerW') AS hp_odu_w,
80
+ gridworks.C2F(AVG (avg_value) FILTER (WHERE channel_name='hp-lwt' AND unit='WaterTempCTimes1000') / 1000) AS hp_avg_lwt,
81
+ gridworks.C2F(AVG (avg_value) FILTER (WHERE channel_name='hp-ewt' AND unit='WaterTempCTimes1000') / 1000) AS hp_avg_ewt,
82
+ gridworks.C2F(AVG (avg_value) FILTER (WHERE channel_name='dist-swt' AND unit='WaterTempCTimes1000') / 1000) AS dist_avg_swt,
83
+ gridworks.C2F(AVG (avg_value) FILTER (WHERE channel_name='dist-rwt' AND unit='WaterTempCTimes1000') / 1000) AS dist_avg_rwt,
84
+ AVG (avg_value) FILTER (where channel_name like '%-relay3') AS relay_3_pulled_fraction,
85
+ AVG (avg_value) FILTER (where channel_name like '%-relay5') AS relay_5_pulled_fraction,
86
+ AVG (avg_value) FILTER (where channel_name like '%-relay6') AS relay_6_pulled_fraction,
87
+ AVG (avg_value) FILTER (where channel_name like '%-relay9') AS relay_9_pulled_fraction,
88
+ AVG (avg_value) FILTER (where channel_name like 'zone1-%-heat-call') AS zone1_heatcall_fraction,
89
+ AVG (avg_value) FILTER (where channel_name like 'zone2-%-heat-call') AS zone2_heatcall_fraction,
90
+ AVG (avg_value) FILTER (where channel_name like 'zone3-%-heat-call') AS zone3_heatcall_fraction,
91
+ AVG (avg_value) FILTER (where channel_name like 'zone4-%-heat-call') AS zone4_heatcall_fraction
92
+ FROM (
93
+ SELECT
94
+ terminal_asset_alias,
95
+ channel_name,
96
+ unit,
97
+ time_bucket,
98
+ interpolated_average(
99
+ time_weight,
100
+ time_bucket,
101
+ '1 hour',
102
+ LAG(time_weight) OVER (PARTITION BY terminal_asset_alias,channel_name,unit ORDER BY time_bucket),
103
+ LEAD(time_weight) OVER (PARTITION BY terminal_asset_alias,channel_name,unit ORDER BY time_bucket)
104
+ ) AS avg_value
105
+ FROM (
106
+ SELECT * FROM gridworks.readings_1hr
107
+ WHERE 1=1
108
+ AND time_bucket >= t_start
109
+ AND time_bucket <= t_end
110
+ )
111
+ )
112
+ GROUP BY time_bucket,terminal_asset_alias
113
+ ORDER BY time_bucket
114
+ ) AS avg_values ON kwh_values.time_bucket = avg_value_time_bucket AND kwh_values.terminal_asset_alias = avg_value_ta
115
+ JOIN (
116
+ SELECT
117
+ terminal_asset_alias AS first_value_ta,
118
+ time_bucket AS first_value_time_bucket,
119
+ AVG (first_val) FILTER (WHERE channel_name='buffer-depth1' AND unit='FahrenheitX100') / 100 AS buffer_depth1_start,
120
+ AVG (first_val) FILTER (WHERE channel_name='buffer-depth2' AND unit='FahrenheitX100') / 100 AS buffer_depth2_start,
121
+ AVG (first_val) FILTER (WHERE channel_name='buffer-depth3' AND unit='FahrenheitX100') / 100 AS buffer_depth3_start,
122
+ AVG (first_val) FILTER (WHERE channel_name='buffer-depth4' AND unit='FahrenheitX100') / 100 AS buffer_depth4_start,
123
+ AVG (first_val) FILTER (WHERE channel_name='tank1-depth1' AND unit='FahrenheitX100') / 100 AS tank1_depth1_start,
124
+ AVG (first_val) FILTER (WHERE channel_name='tank1-depth2' AND unit='FahrenheitX100') / 100 AS tank1_depth2_start,
125
+ AVG (first_val) FILTER (WHERE channel_name='tank1-depth3' AND unit='FahrenheitX100') / 100 AS tank1_depth3_start,
126
+ AVG (first_val) FILTER (WHERE channel_name='tank1-depth4' AND unit='FahrenheitX100') / 100 AS tank1_depth4_start,
127
+ AVG (first_val) FILTER (WHERE channel_name='tank2-depth1' AND unit='FahrenheitX100') / 100 AS tank2_depth1_start,
128
+ AVG (first_val) FILTER (WHERE channel_name='tank2-depth2' AND unit='FahrenheitX100') / 100 AS tank2_depth2_start,
129
+ AVG (first_val) FILTER (WHERE channel_name='tank2-depth3' AND unit='FahrenheitX100') / 100 AS tank2_depth3_start,
130
+ AVG (first_val) FILTER (WHERE channel_name='tank2-depth4' AND unit='FahrenheitX100') / 100 AS tank2_depth4_start,
131
+ AVG (first_val) FILTER (WHERE channel_name='tank3-depth1' AND unit='FahrenheitX100') / 100 AS tank3_depth1_start,
132
+ AVG (first_val) FILTER (WHERE channel_name='tank3-depth2' AND unit='FahrenheitX100') / 100 AS tank3_depth2_start,
133
+ AVG (first_val) FILTER (WHERE channel_name='tank3-depth3' AND unit='FahrenheitX100') / 100 AS tank3_depth3_start,
134
+ AVG (first_val) FILTER (WHERE channel_name='tank3-depth4' AND unit='FahrenheitX100') / 100 AS tank3_depth4_start,
135
+ AVG (first_val) FILTER (where channel_name = 'forecast-oat' AND unit='FahrenheitX100') / 100 AS oat_f,
136
+ AVG (first_val) FILTER (where channel_name = 'forecast-ws' AND unit='MilesPerHourX1000') / 1000 AS ws_mph,
137
+ AVG (first_val) FILTER (where channel_name = 'total-usd-per-mwh' AND unit='DollarsX1000') / 1000 AS total_usd_per_mwh,
138
+ AVG (first_val) FILTER (where channel_name = 'lmp-usd-per-mwh' AND unit='DollarsX1000') / 1000 AS lmp_usd_per_mwh,
139
+ AVG (first_val) FILTER (where channel_name = 'buffer-available-kwh' AND unit='KilowattHoursX1000') / 1000 AS buffer_available_kwh
140
+
141
+ FROM (
142
+ SELECT
143
+ terminal_asset_alias,
144
+ channel_name,
145
+ unit,
146
+ time_bucket,
147
+ first_val(time_weight) AS first_val
148
+ FROM (
149
+ SELECT * FROM gridworks.readings_1hr
150
+ WHERE 1=1
151
+ AND time_bucket >= t_start
152
+ AND time_bucket <= t_end
153
+ ORDER BY time_bucket
154
+ )
155
+ )
156
+ GROUP BY time_bucket,terminal_asset_alias
157
+ ORDER BY time_bucket
158
+ ) AS first_values ON kwh_values.time_bucket = first_value_time_bucket AND kwh_values.terminal_asset_alias = first_value_ta
159
+ OFFSET 0 -- Optimization
160
+ )
161
+ ORDER BY time_bucket
162
+ $BODY$;
@@ -0,0 +1,40 @@
1
+ CREATE FUNCTION gridworks.retrieve_readings_1s(
2
+ t_start timestamp with time zone,
3
+ t_end timestamp with time zone,
4
+ channels text[])
5
+ RETURNS TABLE(terminal_asset_alias text, channel_name text, unit text, unit_type text, time_bucket timestamp with time zone, value numeric)
6
+ STABLE
7
+ LANGUAGE 'sql'
8
+
9
+ AS $BODY$
10
+
11
+ SELECT
12
+ time_values.terminal_asset_alias,
13
+ time_values.channel_name,
14
+ time_values.unit,
15
+ time_values.unit_type,
16
+ time_bucket_gapfill(INTERVAL '1 seconds', time_values.time_bucket) AS time_bucket_gapfilled,
17
+ locf(max(time_values.avg_value)) AS last_reading_value
18
+ FROM (
19
+ SELECT
20
+ rc.terminal_asset_alias,
21
+ rc.name AS channel_name,
22
+ rc.unit,
23
+ rc.unit_type,
24
+ time_bucket(INTERVAL '1 seconds', r.timestamp) AS time_bucket,
25
+ avg(r.value) AS avg_value
26
+ -- time_weight('LOCF', r.timestamp, r.value) AS time_weight
27
+ FROM gridworks.readings r
28
+ JOIN gridworks.reading_channels rc ON rc.id = r.channel_id
29
+ WHERE
30
+ r.timestamp >= t_start --'2026-04-20T04:00:00Z'
31
+ AND r.timestamp <= t_end --'2026-04-20T07:00:00Z'
32
+ AND rc.name = ANY(channels)
33
+ GROUP BY time_bucket, rc.name, rc.unit, rc.unit_type, rc.terminal_asset_alias
34
+ ORDER BY rc.name, time_bucket
35
+ ) AS time_values
36
+ WHERE 1=1
37
+ AND time_values.time_bucket >= t_start
38
+ AND time_values.time_bucket <= t_end
39
+ GROUP BY time_values.terminal_asset_alias, time_values.channel_name, time_values.unit, time_values.unit_type, time_bucket_gapfilled
40
+ $BODY$;
@@ -0,0 +1,15 @@
1
+ CREATE MATERIALIZED VIEW gridworks.readings_1hr
2
+ WITH (timescaledb.continuous)
3
+ AS
4
+ SELECT
5
+ reading_channels.terminal_asset_alias AS terminal_asset_alias,
6
+ reading_channels.name AS channel_name,
7
+ reading_channels.unit AS unit,
8
+ reading_channels.unit_type AS unit_type,
9
+ time_bucket(INTERVAL '1 hour', readings.timestamp) AS time_bucket,
10
+ time_weight('LOCF', readings.timestamp, readings.value) AS time_weight
11
+ FROM gridworks.readings
12
+ JOIN gridworks.reading_channels ON reading_channels.id = readings.channel_id
13
+ GROUP BY time_bucket, reading_channels.terminal_asset_alias, reading_channels.name, reading_channels.unit, reading_channels.unit_type
14
+ ORDER BY reading_channels.terminal_asset_alias, reading_channels.name, time_bucket
15
+ WITH NO DATA;
@@ -0,0 +1,46 @@
1
+ import logging
2
+ from datetime import datetime, timedelta, timezone
3
+ import dotenv
4
+
5
+ from sqlalchemy import create_engine, text
6
+ from sqlalchemy.orm import sessionmaker
7
+
8
+ from gw_data.config import Settings
9
+
10
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
11
+ logger = logging.getLogger(__name__)
12
+
13
+ dotenv.load_dotenv()
14
+ settings = Settings()
15
+ engine = create_engine(settings.db_url.get_secret_value(), echo=False)
16
+
17
+ db_sessionmaker = sessionmaker(bind=engine)
18
+ db_session = db_sessionmaker()
19
+
20
+
21
+ # t_start = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
22
+ t_start = datetime(2026, 6, 1, tzinfo=timezone.utc)
23
+ t_final = datetime(2026, 1, 31, tzinfo=timezone.utc)
24
+
25
+ # t_start = datetime(2026, 4, 22, tzinfo=timezone.utc)
26
+ # t_final = datetime(2026, 4, 18, tzinfo=timezone.utc)
27
+
28
+
29
+ while t_start > t_final:
30
+ logger.info(f'Refreshing from {t_start.isoformat()}...')
31
+ t_end = t_start
32
+ t_start = t_end - timedelta(days=1)
33
+
34
+ stmt = text(f"""
35
+ INSERT INTO gridworks.cached_hourly_data
36
+ WITH hourly_data AS MATERIALIZED (
37
+ SELECT * FROM gridworks.calc_hourly_data(t_start => '{(t_start - timedelta(hours=1)).isoformat()}', t_end => '{t_end.isoformat()}')
38
+ )
39
+ SELECT * FROM hourly_data
40
+ WHERE time_bucket >= '{t_start.isoformat()}' AND time_bucket < '{t_end.isoformat()}';
41
+ """)
42
+
43
+ db_result = db_session.execute(stmt)
44
+ db_session.commit()
45
+ logger.info(f'Inserted {db_result.rowcount} rows')
46
+
@@ -4,3 +4,4 @@
4
4
  "fir","{""street"": ""7890 Fir Road"", ""city"": ""Millinocket"", ""state"": ""ME"", ""zip"": ""00130"", ""country"": ""USA"", ""latitude"": 45.64500, ""longitude"": -68.72}","{""first_name"": ""Renee"", ""last_name"": ""Fir"", ""phone"": ""207-754-3256"", ""email"": ""reneefir@example.com""}","{""first_name"": ""Kim"", ""last_name"": ""FirFir"", ""phone"": ""207-210-0452"", ""email"": null}","null",3,"hw1.isone.me.versant.keene.fir","{""status"": ""ok"", ""message"": null, ""acked"": null, ""acked_by"": null, ""acked_at"": null}","""NotListeningToAtn""","127.0.0.1","unknown","{""alpha"": 7.5, ""beta"": -0.13, ""gamma"": 0.0022, ""intermediate_power_kw"": 1.5, ""intermediate_rswt"": 94.0, ""dd_power_kw"": 7.5, ""dd_rswt"": 150.0, ""dd_delta_t"": 20.0}","2026-01-23 07:04:32.477627-06"
5
5
  "maple","{""street"": ""101 Maple Road"", ""city"": ""Millinocket"", ""state"": ""ME"", ""zip"": ""04462"", ""country"": ""USA"", ""latitude"": 45.64400, ""longitude"": -68.73}","{""first_name"": ""Herman"", ""last_name"": ""Maple"", ""phone"": ""555-444-3333"", ""email"": null}","null","null",4,"hw1.isone.me.versant.keene.maple","{""status"": ""ok"", ""message"": null, ""acked"": null, ""acked_by"": null, ""acked_at"": null}","""NotListeningToAtn""","127.0.0.1","unknown","{""alpha"": 6.3, ""beta"": -0.09, ""gamma"": 0.0007, ""intermediate_power_kw"": 1.5, ""intermediate_rswt"": 118.0, ""dd_power_kw"": 6.3, ""dd_rswt"": 160.0, ""dd_delta_t"": 20.0}","2026-01-23 07:04:32.477627-06"
6
6
  "elm","{""street"": ""202 Elm Lane"", ""city"": ""Millinocket"", ""state"": ""ME"", ""zip"": ""04462"", ""country"": ""USA"", ""latitude"": 45.6800, ""longitude"": -68.71}","{""first_name"": ""Mike"", ""last_name"": ""Elm"", ""phone"": ""555-666-7777"", ""email"": null}","{""first_name"": ""Michelle"", ""last_name"": ""Elm"", ""phone"": null, ""email"": null}","null",5,"hw1.isone.me.versant.keene.elm","{""status"": ""ok"", ""message"": null, ""acked"": null, ""acked_by"": null, ""acked_at"": null}","""NotListeningToAtn""","127.0.0.1","unknown","{""alpha"": 7.0, ""beta"": -0.13, ""gamma"": 0.001, ""intermediate_power_kw"": 1.5, ""intermediate_rswt"": 120.0, ""dd_power_kw"": 7.0, ""dd_rswt"": 150.0, ""dd_delta_t"": 20.0}","2026-01-23 07:04:32.477627-06"
7
+ "spruce","{""street"": ""303 Spruce Blvd"", ""city"": ""Millinocket"", ""state"": ""ME"", ""zip"": ""04462"", ""country"": ""USA"", ""latitude"": 45.6200, ""longitude"": -68.70}","{""first_name"": ""Gerald"", ""last_name"": ""Spruce"", ""phone"": ""555-666-8888"", ""email"": null}","{""first_name"": ""Maryann"", ""last_name"": ""Spruce"", ""phone"": null, ""email"": null}","null",5,"hw1.isone.me.versant.keene.spruce","{""status"": ""ok"", ""message"": null, ""acked"": null, ""acked_by"": null, ""acked_at"": null}","""NotListeningToAtn""","127.0.0.1","unknown","{""alpha"": 7.0, ""beta"": -0.13, ""gamma"": 0.001, ""intermediate_power_kw"": 1.5, ""intermediate_rswt"": 120.0, ""dd_power_kw"": 7.0, ""dd_rswt"": 150.0, ""dd_delta_t"": 20.0}","2026-01-23 07:04:32.477627-06"
@@ -0,0 +1,70 @@
1
+ -- Find readings for specific channels, installations, timeframe
2
+ select rc.terminal_asset_alias,rc.name,r.timestamp,r.value
3
+ from gridworks.readings r
4
+ join gridworks.reading_channels rc on r.channel_id = rc.id
5
+ where 1=1
6
+ and (rc.name like '%whitewire%' or rc.name like '%heat-call%')
7
+ and rc.terminal_asset_alias like '%beech%'
8
+ and timestamp >= '2026-04-20T04:00:00Z'
9
+ and timestamp <= '2026-04-20T05:00:00Z'
10
+ order by r.timestamp,rc.terminal_asset_alias
11
+ limit 100;
12
+
13
+ -- Find channels for an installation
14
+ select * from gridworks.reading_channels
15
+ where terminal_asset_alias like '%beech%'
16
+ order by name,terminal_asset_alias;
17
+
18
+ -- Find messages for an installation & timeframe
19
+ select * from gridworks.messages
20
+ where 1=1
21
+ and from_alias like '%beech%'
22
+ and timestamp >= '2026-04-20T04:00:00Z'
23
+ and timestamp <= '2026-04-20T05:00:00Z'
24
+ limit 100;
25
+
26
+ -- Total database size
27
+ SELECT pg_size_pretty(pg_database_size('tsdb')) AS size;
28
+
29
+ -- Display the size of all database tables in order.
30
+ -- This will show each TimescaleDB chunk as a separate table.
31
+ SELECT
32
+ table_schema || '.' || table_name AS table_full_name,
33
+ pg_size_pretty(pg_total_relation_size('"' || table_schema || '"."' || table_name || '"')) AS size
34
+ FROM information_schema.tables
35
+ ORDER BY
36
+ pg_total_relation_size('"' || table_schema || '"."' || table_name || '"') DESC;
37
+
38
+ -- Display compression stats for each chunk in the readings table
39
+ SELECT
40
+ chunk_name,
41
+ pg_size_pretty(before_compression_total_bytes) AS size_before,
42
+ pg_size_pretty(after_compression_total_bytes) AS size_after,
43
+ 100 - (after_compression_total_bytes::float / before_compression_total_bytes * 100) AS compression_ratio_pct
44
+ FROM chunk_compression_stats('gridworks.readings');
45
+
46
+ -- Get info (table, time range, etc.) about the TimescaleDB chunks.
47
+ SELECT * FROM timescaledb_information.chunks
48
+
49
+ -- Display the size in MB of our two main tables across all its chunks
50
+ SELECT
51
+ pg_size_pretty(hypertable_size('gridworks.readings')) as "Readings Table Size",
52
+ pg_size_pretty(hypertable_size('gridworks.messages')) as "Messages Table Size";
53
+
54
+ -- Detailed size for a full hypertable across all its chunks
55
+ SELECT
56
+ pg_size_pretty(table_bytes) AS data_size,
57
+ pg_size_pretty(index_bytes) AS index_size,
58
+ pg_size_pretty(toast_bytes) AS toast_size,
59
+ pg_size_pretty(total_bytes) AS total_size
60
+ FROM hypertable_detailed_size('gridworks.readings');
61
+
62
+ -- Info about timescaledb jobs
63
+ select * from timescaledb_information.jobs;
64
+
65
+ -- Manually run a job (e.g. run compression after an import).
66
+ -- Fill in job_id based on the query above
67
+ CALL run_job(job_id);
68
+
69
+
70
+
@@ -103,7 +103,7 @@ wheels = [
103
103
 
104
104
  [[package]]
105
105
  name = "gw-data"
106
- version = "0.3.0"
106
+ version = "0.3.2"
107
107
  source = { editable = "." }
108
108
  dependencies = [
109
109
  { name = "alembic" },
@@ -1,44 +0,0 @@
1
- CREATE VIEW readings_pretty AS
2
- SELECT
3
- dc.name AS name,
4
- r.value AS value,
5
- dc.telemetry_name AS telemetry_name,
6
- timestamp AS time,
7
- r.message_id AS message_id
8
- FROM
9
- readings r
10
- JOIN
11
- data_channels dc
12
- ON
13
- r.data_channel_id = dc.id;
14
-
15
-
16
-
17
- SELECT * FROM preadings WHERE name LIKE '%buffer%' LIMIT 5;
18
-
19
-
20
- CREATE OR REPLACE VIEW msg_pretty AS
21
- SELECT
22
- payload, from_alias, message_type_name, persisted_at AS message_persisted, created_at AS message_created
23
- FROM messages;
24
-
25
-
26
-
27
-
28
- SELECT COUNT(*) from messages;
29
-
30
- SELECT COUNT(*) FROM messages WHERE message_type_name = 'power.watts';
31
-
32
- SELECT from_alias, message_type_name, message_created FROM msg_pretty WHERE message_type_name LIKE '%status%' LIMIT 5;
33
-
34
- SELECT pg_size_pretty(pg_database_size('gridworks')) AS size;
35
- SELECT * FROM msg_pretty WHERE message_type_name LIKE '%param%';
36
-
37
- SELECT id, from_alias, message_type_name, persisted_at FROM messages WHERE message_type_name LIKE '%param%';
38
-
39
- DELETE FROM messages WHERE id = 'bf06df07-36a5-4e54-ae64-37dea6041ea8';
40
-
41
- SELECT MIN(time) AS earliest, MAX(time) AS latest FROM readings_pretty;
42
- SELECT MIN(message_created) AS earliest, MAX(message_created) AS latest FROM msg_pretty;
43
-
44
- SELECT from_alias, message_type_name, message_created FROM msg_pretty WHERE message_type_name LIKE '%batched%' LIMIT 5;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes