-
Notifications
You must be signed in to change notification settings - Fork 36
Add Okiff and GREENNET distributors and the CUBA IoT Platform case study #643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
827c167
b42b4eb
2d73ebe
94c824a
fe6f1f6
3ca1a5a
57e3518
dd7ff90
19655d7
1976503
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import type { CaseStudyData } from './types'; | ||
|
|
||
| export const data: CaseStudyData = { | ||
| title: 'Backup power IoT in Kazakhstan: how CUBA IoT Platform monitors diesel generators with ThingsBoard', | ||
| pageTitle: 'How CUBA IoT Platform Monitors Diesel Generators', | ||
| description: | ||
| 'CUBA IoT Platform connects diesel generator controllers to ThingsBoard via CUBA RePort gateways: fleet dashboards, alarms, fuel and readiness monitoring.', | ||
| pageSlug: 'cuba-iot-platform', | ||
| breadcrumb: 'CUBA IoT Platform — Facility Management', | ||
| categories: ['Facility management'], | ||
| logoIsLightOnDark: true, | ||
|
|
||
| hero: { | ||
| category: 'FACILITY MANAGEMENT', | ||
| heading: 'Backup power IoT in Kazakhstan: how CUBA IoT Platform monitors diesel generators with ThingsBoard', | ||
| paragraphs: [ | ||
| 'CUBA IoT Platform is a Kazakhstan-based IoT company and local ThingsBoard integrator. The company delivers online monitoring, real-time device control, data visualization, analytics, automation scenarios, and turnkey integration for distributed infrastructure. CUBA combines platform implementation, local engineering support, and proprietary hardware to help customers monitor equipment, reduce operational risk, and respond to events faster.', | ||
| 'For backup power operators, diesel generator units are critical but often geographically distributed assets. A generator may remain idle for long periods, but it must start immediately when the main power supply fails. CUBA’s diesel generator monitoring solution connects generator controllers through CUBA RePort gateways and uses ThingsBoard dashboards, alarms, telemetry history, and rule chains to give operators continuous visibility into generator readiness.', | ||
| ], | ||
| logo: '/images/case-studies/cuba-iot-platform-logo.png', | ||
| logoAlt: 'CUBA IoT Platform logo', | ||
| backgroundImage: '/images/case-studies/cuba-iot-platform.webp', | ||
| }, | ||
|
|
||
| statistics: [ | ||
| { value: 100, suffix: '+', label: 'monitored generator nodes in production' }, | ||
| { value: 74, suffix: '+', label: 'telemetry and alarm parameters per generator profile' }, | ||
| { value: 4, suffix: '+', label: 'supported generator controller/OEM families' }, | ||
| ], | ||
|
|
||
| quote: { | ||
| text: 'With ThingsBoard and CUBA RePort gateways, diesel generators are no longer rarely inspected backup assets. Operators can see low fuel, discharged batteries, abnormal temperatures, fault states, missed test runs, and maintenance needs before they become outages.', | ||
| author: 'Dmitry Shkunov', | ||
| role: 'CTO at Skif Trade LLP (SMALL, SPAR supermarkets)', | ||
| }, | ||
|
|
||
| problem: { | ||
| description: | ||
| 'Organizations relying on diesel generators needed reliable visibility into generator readiness, actual runtime, alarms, fuel level, and maintenance status. Manual inspection could miss critical issues such as discharged batteries, low fuel, incorrect operating mode, heater failure, cooling problems, or unreported controller alarms. CUBA addressed these challenges by connecting generator controllers to ThingsBoard and converting raw telemetry into dashboards, alarms, historical trends, and operational notifications.', | ||
| challenges: [ | ||
| 'Limited visibility into geographically distributed diesel generators and their actual readiness', | ||
| 'Manual checks did not reliably detect low fuel, battery voltage issues, incorrect operating modes, or controller fault states', | ||
| 'Service teams needed maintenance planning based on engine hours, start counts, and real operating history instead of fixed assumptions', | ||
| 'Operators needed immediate notifications when a generator failed, fuel changed unexpectedly, or a scheduled test run was missed', | ||
| ], | ||
| results: [ | ||
| 'Continuous monitoring of fuel level, battery voltage, oil pressure, coolant temperature, generator/mains electrical parameters, runtime, start count, operating mode, and alarms', | ||
| 'Centralized fleet table with sorting by site, generator type, operating status, fuel level, alarm count, coolant temperature, and control mode', | ||
| 'Rule-based alarm activation and instant notifications through Telegram, SMS, and email', | ||
| 'Historical trends for outage investigation, fuel refill tracking, generator start/stop analysis, maintenance planning, and operational reporting', | ||
| ], | ||
| }, | ||
|
|
||
| power: { | ||
| companyName: 'CUBA IoT Platform', | ||
| blocks: [ | ||
| { | ||
| title: 'Gateway-based data acquisition', | ||
| text: 'CUBA RePort polls diesel generator controllers over Modbus RTU or Modbus TCP and transfers telemetry to the platform through MQTT/HTTP. The gateway supports Ethernet, GSM LTE, Wi-Fi, RS-485, UART, NTP time synchronization, built-in web configuration, DIN-rail mounting, and backup autonomy of at least 20 minutes. This makes the solution suitable for distributed sites where wired connectivity may be unavailable or unreliable.', | ||
| image: '/images/case-studies/cuba-iot-platform-1.webp', | ||
| imageAlt: 'CUBA RePort gateway that connects diesel generator controllers to ThingsBoard', | ||
| }, | ||
| { | ||
| title: 'Unified asset model and dashboards', | ||
| text: 'ThingsBoard is configured around generator assets, sites, telemetry streams, alarms, and operator dashboards. Dispatchers can start from a fleet-level table, sort assets by status or alarm count, and drill down to a detailed generator page. The detailed panel consolidates engine state, generator electrical parameters, mains parameters, breaker states, runtime, and control mode.', | ||
| image: '/images/case-studies/cuba-iot-platform-2.webp', | ||
| imageAlt: 'CUBA RePort generator monitoring dashboard with engine, generator, and mains panels', | ||
| }, | ||
| { | ||
| title: 'Rule Engine alarms and operational notifications', | ||
| text: 'Rule chains process controller alarms, fuel level, battery voltage, coolant temperature, oil pressure, operating mode, and generator/mains state. Notifications are routed to responsible users through Telegram, SMS, and email. Each event can include the site, generator, source gateway, parameter, value, and timestamp, reducing the need for manual dashboard checks.', | ||
| image: '/images/case-studies/cuba-iot-platform-3.webp', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-raising the part of my earlier image note that the swap didn't cover. Blocks 2 and 3 now hold the right screenshots relative to each other, but this block is specifically about rule chains, alarm activation and Telegram/SMS/email notifications, and the image it lands on is four historical trend charts (coolant temperature, oil pressure, mains power, battery voltage) with an "Engine stop" annotation — no alarm, no rule chain, no notification anywhere in the frame. So the strongest claim in the section is the one with no supporting visual. A rule-chain screenshot, an alarms table, or a Telegram/email notification capture would carry real weight here. If nothing like that is obtainable from the customer, dropping the image from this block would still read better than an unrelated one. |
||
| imageAlt: 'Generator and mains telemetry charts in a ThingsBoard dashboard', | ||
| }, | ||
| { | ||
| title: 'Historical analytics and service workflows', | ||
| text: 'Time-series widgets show current and historical data for any selected period. Operators can analyze mains outages, generator starts, fuel refills, engine stops, voltage, current, power, oil pressure, coolant temperature, and battery behavior. This supports readiness proof, outage investigation, fuel theft detection, and maintenance planning based on actual runtime.', | ||
| image: '/images/case-studies/cuba-iot-platform-4.webp', | ||
| imageAlt: 'Historical charts of fuel level, coolant temperature, oil pressure, and power', | ||
| }, | ||
| { | ||
| title: 'ThingsBoard implementation', | ||
| text: 'CUBA deployed ThingsBoard as the central monitoring layer for diesel generator assets. CUBA RePort gateways poll generator controllers over industrial protocols and publish structured telemetry to the platform. ThingsBoard stores telemetry, manages assets and dashboards, processes alarm rules, and sends notifications to responsible users.', | ||
| image: '/images/case-studies/cuba-iot-platform-5.webp', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other re-raise, now more visible than it was before the swap. Comparing the four screenshots: image 4 is the full 12-tile historical dashboard, and images 3 and 5 are both crops of that same dashboard — image 3 is its Coolant Temperature / Oil Pressure / Mains power / Battery Voltage tiles, image 5 its Mains current / Mains power / Generator current / Generator power tiles. Blocks 3, 4 and 5 therefore render three overlapping views of one dashboard, with image 4 containing both of the others. What makes it worth acting on rather than filing as a nit is what the cropping costs. In image 5 the whole top row (Mains current, Mains power) sits flat at zero. In image 4 that same flat line is legible, because the annotation "Power network shutdown" is right there explaining it — mains died, which is the whole reason the generator ran. Cropped away from that annotation it just reads as an empty chart, so the crop loses the one thing that made the data tell a story. And this block is the retitled architecture block — gateways polling controllers over Modbus and publishing telemetry. A dashboard crop, half of it flat, is a weak illustration for it; an architecture or topology diagram is the natural fit and would also break up three consecutive chart grids. If the design supplies exactly these five crops then this is a question for the designer rather than a change to make here — but with block 2's screenshot re-exported already, 3/4/5 is now the weakest part of the page's visuals. |
||
| imageAlt: 'Mains and generator current and power charts in ThingsBoard', | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| help: { | ||
| industryName: 'backup power operations improve reliability with IoT', | ||
| blocks: [ | ||
| { | ||
| title: 'Early detection of generator issues', | ||
| text: 'ThingsBoard helps detect problems before they lead to generator failure. Operators receive alerts when battery voltage drops, fuel level becomes too low, coolant temperature rises, oil pressure changes, or the controller reports an alarm.', | ||
| images: [ | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/sensors-widget.webp', | ||
| alt: 'ThingsBoard sensors widget', | ||
| title: 'ThingsBoard sensors widget', | ||
| }, | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/entities-table.webp', | ||
| alt: 'ThingsBoard entities table widget', | ||
| title: 'ThingsBoard entities table widget', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| title: 'Reliable start readiness monitoring', | ||
| text: 'The platform continuously shows whether each generator is ready to start during a power outage. This helps identify common hidden risks such as discharged batteries, failed preheating, fuel shortage, manual mode, or active controller faults.', | ||
| images: [ | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/tables.webp', | ||
| alt: 'ThingsBoard tables widget', | ||
| title: 'ThingsBoard tables widget', | ||
| }, | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/bar-chart.webp', | ||
| alt: 'ThingsBoard bar chart widget', | ||
| title: 'ThingsBoard bar chart widget', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| title: 'Event-based maintenance control', | ||
| text: 'Runtime hours, number of starts, alarms, and operating history are collected automatically. Service teams can plan maintenance based on real equipment usage and react faster when abnormal operation is detected.', | ||
| images: [ | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/line-chart.webp', | ||
| alt: 'ThingsBoard line chart widget', | ||
| title: 'ThingsBoard line chart widget', | ||
| }, | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/alarms-table.webp', | ||
| alt: 'ThingsBoard alarms table widget', | ||
| title: 'ThingsBoard alarms table widget', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| title: 'Fuel level and usage visibility', | ||
| text: 'Fuel data is available remotely for every monitored generator. This helps detect sudden drops, refueling events, abnormal consumption, and possible fuel theft.', | ||
| images: [ | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/pie-chart.webp', | ||
| alt: 'ThingsBoard pie chart widget', | ||
| title: 'ThingsBoard pie chart widget', | ||
| }, | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/battery-level.webp', | ||
| alt: 'ThingsBoard battery level widget', | ||
| title: 'ThingsBoard battery level widget', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| title: 'Instant notifications for responsible teams', | ||
| text: 'ThingsBoard sends alerts through configured notification channels when critical events occur. This reduces response time and allows service teams to act before the backup power system becomes unavailable.', | ||
| images: [ | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/alarms-table-2.webp', | ||
| alt: 'ThingsBoard alarms table widget', | ||
| title: 'ThingsBoard alarms table widget', | ||
| }, | ||
| { | ||
| src: 'https://img.thingsboard.io/case-studies/notification-widget.webp', | ||
| alt: 'ThingsBoard notification widget', | ||
| title: 'ThingsBoard notification widget', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
|
|
||
| contact: { | ||
| companyLogo: '/images/case-studies/cuba-iot-platform-logo.png', | ||
| companyLogoAlt: 'CUBA IoT Platform logo', | ||
| companyLogoWidth: 240, | ||
| companyLogoHeight: 91, | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ export const caseStudyCategories = [ | |
| // 2. Add the slug to this array in the position you want it displayed. | ||
| // 3. (Optional) If the card needs a custom logo height, add an entry to | ||
| // `cardLogoHeights` below. | ||
| // 4. (Optional) Logo already light-on-dark? Set `logoIsLightOnDark: true` in the data file. | ||
| export const caseStudyOrder: string[] = [ | ||
| 'ibt-systems', | ||
| 'acte-technology', | ||
|
|
@@ -29,6 +30,8 @@ export const caseStudyOrder: string[] = [ | |
| 'onedata', | ||
| 'lumen', | ||
| 'solandtec', | ||
| // Intentionally out of newest-first order: first card of catalog page 2. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still unresolved: the fragility half is fixed — the client script reads One clause would close it — why page 2 specifically, rather than the top of page 1 where a new case study would normally go? That reasoning is the only thing stopping someone from "fixing" the ordering later. (No need to put the |
||
| 'cuba-iot-platform', | ||
| 'tjk-solutions', | ||
| 'energenix', | ||
| 'energroup', | ||
|
|
@@ -61,7 +64,8 @@ export const caseStudyOrder: string[] = [ | |
|
|
||
| // Card-only logo height overrides. The hero on the detail page may want a | ||
| // different intrinsic size than the catalog card, so we keep the card override | ||
| // here rather than on the data itself. | ||
| // here rather than on the data itself. Sizing is per surface; the logo's | ||
| // treatment (`logoIsLightOnDark`) spans surfaces, so that one lives on the data. | ||
| const cardLogoHeights: Record<string, number> = { | ||
| ariot: 75, | ||
| iioote: 35, | ||
|
|
@@ -120,6 +124,7 @@ function toCard(d: CaseStudyData): CaseStudyCard { | |
| logo: d.hero.logo, | ||
| logoAlt: d.hero.logoAlt, | ||
| logoHeight: cardLogoHeights[d.pageSlug], | ||
| logoIsLightOnDark: d.logoIsLightOnDark, | ||
| backgroundImage: d.hero.backgroundImage, | ||
| }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,10 +18,19 @@ export function getCoverage(d: Distributor): string[] { | |
| return Array.from(new Set(d.regions.flatMap((r) => REGION_MEMBERSHIP[r]))).sort(); | ||
| } | ||
|
|
||
| /** Regions the table files a country under. */ | ||
| function regionsOf(country: string, membership: Record<Region, string[]>): Region[] { | ||
| return (Object.keys(membership) as Region[]).filter((r) => membership[r].includes(country)); | ||
| } | ||
|
|
||
| /** | ||
| * The region table and the distributor data must describe the same countries. | ||
| * A named country the table doesn't classify silently narrows region-wide | ||
| * coverage; a classified country nobody covers is dead weight that hides typos. | ||
| * The region table and the distributor data must describe the same countries, | ||
| * and every distributor must declare every region its named countries fall | ||
| * under. A named country the table doesn't classify silently narrows | ||
| * region-wide coverage; a classified country nobody covers is dead weight that | ||
| * hides typos; a named country in an undeclared region can't be reached through | ||
| * that region's filter, since the dropdown offers only the region's own | ||
| * countries and a card only matches regions it declares. | ||
| */ | ||
| function findCoverageErrors(distributors: Distributor[], membership: Record<Region, string[]>): string[] { | ||
| const named = new Set(distributors.flatMap(getNamedCountries)); | ||
|
|
@@ -35,6 +44,19 @@ function findCoverageErrors(distributors: Distributor[], membership: Record<Regi | |
| ); | ||
| } | ||
|
|
||
| const undeclared = distributors.flatMap((d) => { | ||
| // Unclassified countries are already reported above. | ||
| const outside = getNamedCountries(d).filter( | ||
| (c) => classified.has(c) && regionsOf(c, membership).some((r) => !d.regions.includes(r)) | ||
| ); | ||
| return outside.length > 0 ? [`${d.name} (${outside.join(', ')})`] : []; | ||
| }); | ||
| if (undeclared.length > 0) { | ||
| errors.push( | ||
| `countries in a region their distributor doesn't declare — add that region to the entry in src/data/partners/distributors.ts, or fix the country's classification in src/data/partners/regions.ts: ${undeclared.join('; ')}` | ||
| ); | ||
| } | ||
|
|
||
| // Compares names, not coverage: a region-wide distributor covers these without naming them. | ||
| const stray = [...classified].filter((c) => !named.has(c)).sort(); | ||
| if (stray.length > 0) { | ||
|
|
@@ -52,10 +74,31 @@ function findCoverageErrors(distributors: Distributor[], membership: Record<Regi | |
| return errors; | ||
| } | ||
|
|
||
| /** | ||
| * Declared regions that contain none of the entry's named countries. Such a | ||
| * card shows under the region's "All countries" view but disappears as soon as | ||
| * any country is picked, so this is worth a look — but whether the region is a | ||
| * genuine claim or a leftover is a data-owner call, hence a warning, not an error. | ||
| */ | ||
| function findDanglingRegions(distributors: Distributor[], membership: Record<Region, string[]>): string[] { | ||
| return distributors.flatMap((d) => { | ||
| const countries = getNamedCountries(d); | ||
| if (countries.length === 0) return []; | ||
| const dangling = d.regions.filter((r) => !countries.some((c) => membership[r].includes(c))); | ||
| return dangling.length > 0 ? [`${d.name} → ${dangling.join(', ')}`] : []; | ||
| }); | ||
| } | ||
|
|
||
| /** Throws if the dataset and the region table disagree. Called by `distributors.ts`. */ | ||
| export function assertDistributorData(distributors: Distributor[], membership: Record<Region, string[]>): void { | ||
| const errors = findCoverageErrors(distributors, membership); | ||
| if (errors.length > 0) { | ||
| throw new Error(`[distributors] ${errors.join(' | ')}`); | ||
| } | ||
| const dangling = findDanglingRegions(distributors, membership); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ran this against the dataset as it stands on this branch and it fires immediately, for 8 entries: A check that's noisy on the day it lands stops carrying signal within a build or two, and then it won't catch the case it was written for. Six of the eight are the same shape — a Middle-East-only country (Saudi Arabia, Syria, UAE, Israel) with a leftover I simulated dropping every dangling region to see what it would cost, and mechanically it costs nothing: So the only real question is editorial: should those 8 cards keep showing under a region whose countries they don't serve, in the "All countries" view? That's a data-owner call, which is exactly why a warning rather than an error is the right shape here — I'd just rather the channel started silent. Clearing the HADEX one (see my note in If some of those claims turn out to be deliberate, there's a good in-repo precedent for saying so: |
||
| if (dangling.length > 0) { | ||
| console.warn( | ||
| `[distributors] regions declared without any named country in them (card vanishes once a country is picked): ${dangling.join('; ')}` | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think images 2 and 3 are paired with the wrong blocks. This block's text is about the asset model, the fleet table and the per-generator detail panel that "consolidates engine state, generator electrical parameters, mains parameters, breaker states, runtime, and control mode" — that is precisely what
cuba-iot-platform-3.webpshows (left rail with Engine/Generator readings, Generator and Mains tables, breaker indicators).cuba-iot-platform-2.webpis four historical trend charts with an "Engine stop" annotation, which belongs with block 4. Your own alt strings say as much: image 2's is "Generator telemetry charts", image 3's is "…engine, generator, and mains panels".Caveat: you built this 1:1 from the design, so it's quite possible the pairing is faithful and the mismatch originated there. In that case this is a question for the designer rather than something to fix in the PR — but the mismatch is real either way, so it's worth resolving before it ships.
Two related observations while you're here. Images 2 and 5 are both crops of the same historical dashboard that image 4 shows in full, so blocks 2, 4 and 5 currently render three near-identical chart grids — and a straight 2↔3 swap would leave the Rule Engine block holding image 2, which still shows no alarm or notification. A rule-chain or Telegram/e-mail notification screenshot would carry more weight there. Separately, image 3 has a Russian-language UI ("Карта" / "Таблица") on an otherwise English marketing page — worth an English capture if one is obtainable.